<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: KazKN</title>
    <description>The latest articles on DEV Community by KazKN (@datakaz).</description>
    <link>https://dev.to/datakaz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3771950%2F1f07ca19-8ac3-4a7e-99ea-11c6d02acbb1.png</url>
      <title>DEV Community: KazKN</title>
      <link>https://dev.to/datakaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/datakaz"/>
    <language>en</language>
    <item>
      <title>Turn a Zalo Export into Clean pandas DataFrames</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 16:23:26 +0000</pubDate>
      <link>https://dev.to/datakaz/turn-a-zalo-export-into-clean-pandas-dataframes-49c</link>
      <guid>https://dev.to/datakaz/turn-a-zalo-export-into-clean-pandas-dataframes-49c</guid>
      <description>&lt;p&gt;Zalo export records do not belong in one flat table. A group is not a member, a&lt;br&gt;
member is not a profile, and a message is not a conversation summary. Flattening&lt;br&gt;
all five record types into one spreadsheet creates duplicate columns and hides&lt;br&gt;
the relationships your analysis needs.&lt;/p&gt;

&lt;p&gt;This tutorial retrieves an Apify Dataset, builds one pandas DataFrame per record&lt;br&gt;
type, joins group members to visible profiles, and writes a multi-sheet Excel&lt;br&gt;
workbook.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7RLJMDQBHqo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I built the Zalo Data Exporter used below. I may earn revenue&lt;br&gt;
from Apify when the Actor is run through links in this article, at no extra&lt;br&gt;
cost to you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Install the data tools
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;apify-client pandas openpyxl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the Apify token outside the notebook or script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-token-from-apify-console"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first connection for a saved account alias still requires a temporary QR&lt;br&gt;
approval in Apify Console. Once connected, later calls can reuse the encrypted&lt;br&gt;
login while the session remains valid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_pandas_pipeline_top" rel="noopener noreferrer"&gt;Open the Zalo Data Exporter&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Retrieve the Dataset
&lt;/h2&gt;

&lt;p&gt;Start with a bounded group roster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;


&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kazkn/zalo-member-profile-exporter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exportMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupRoster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxGroups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxMembersPerGroup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;list_items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dataset records: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result can contain &lt;code&gt;profile&lt;/code&gt;, &lt;code&gt;group&lt;/code&gt;, &lt;code&gt;groupMember&lt;/code&gt;, &lt;code&gt;conversation&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;message&lt;/code&gt; rows. The selected preset determines which families appear.&lt;/p&gt;
&lt;h2&gt;
  
  
  Build one DataFrame per record type
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;record_types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupMember&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;record_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;record_type&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record_type&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record_types&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;record_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;An empty DataFrame is expected when the preset did not request that record&lt;br&gt;
family. Do not treat it as an extraction error without checking the chosen&lt;br&gt;
preset and the run summary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Preserve IDs as strings
&lt;/h2&gt;

&lt;p&gt;Spreadsheet tools sometimes convert long identifiers into floating-point&lt;br&gt;
numbers or scientific notation. Cast identifiers before saving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;id_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupMember&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversationId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messageId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;threadId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;senderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;id_columns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;record_type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing values remain nullable strings. That matters for profile data because&lt;br&gt;
Zalo does not expose every field for every visible account.&lt;/p&gt;
&lt;h2&gt;
  
  
  Join group members to visible profiles
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;groupMember&lt;/code&gt; table keeps group membership. The &lt;code&gt;profile&lt;/code&gt; table holds&lt;br&gt;
profile fields that were visible during the run. Join them on &lt;code&gt;userId&lt;/code&gt; without&lt;br&gt;
dropping members that have no profile row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupMember&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;profile_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zaloName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avatarUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relationship&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;profiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;enriched_members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;profiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;profile_columns&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;suffixes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;many_to_one&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;enriched_members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The left join is deliberate. A member row proves visible membership in a joined&lt;br&gt;
group. A missing profile field only means that the field was not returned in&lt;br&gt;
that run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keep completeness with the analytical table
&lt;/h2&gt;

&lt;p&gt;Group and conversation records can carry status fields explaining truncation or&lt;br&gt;
the boundary reached by the read. Preserve those fields in any table used for&lt;br&gt;
reporting.&lt;/p&gt;

&lt;p&gt;For chat analysis, inspect conversation rows before counting messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;conversations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;status_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversationId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completeness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;terminationReason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;conversations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;conversation_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conversations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;status_columns&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A conversation that stopped at &lt;code&gt;PARTIAL_LIMIT&lt;/code&gt; should not be reported as a full&lt;br&gt;
history. The configured message count, date boundary, Zalo Web response, or a&lt;br&gt;
safety ceiling can stop the read.&lt;/p&gt;
&lt;h2&gt;
  
  
  Write separate Excel sheets
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sheet_frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Group members&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;enriched_members&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Groups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Profiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Conversations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ExcelWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zalo-export.xlsx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openpyxl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sheet_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sheet_frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_excel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sheet_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sheet_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This layout is easier to audit than one wide sheet. It also maps directly to&lt;br&gt;
the focused Dataset views exposed by the Actor.&lt;/p&gt;
&lt;h2&gt;
  
  
  Add a small validation report
&lt;/h2&gt;

&lt;p&gt;Before sending the workbook to another team, record the row counts and duplicate&lt;br&gt;
checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;validation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;record_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;enriched_members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;duplicate_member_keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;enriched_members&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;duplicated&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;duplicate_member_keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Duplicate group-member record keys:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duplicate_member_keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validation output does not prove universal completeness. It tells you what&lt;br&gt;
this bounded run delivered and whether your local transformation introduced&lt;br&gt;
obvious duplicates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data pipeline rules that prevent bad reports
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep &lt;code&gt;recordType&lt;/code&gt; in the raw landing table.&lt;/li&gt;
&lt;li&gt;Preserve identifier columns as strings.&lt;/li&gt;
&lt;li&gt;Use left joins when enriching membership rows.&lt;/li&gt;
&lt;li&gt;Carry completeness and termination fields into chat reports.&lt;/li&gt;
&lt;li&gt;Store the source run ID and collection time with each delivery.&lt;/li&gt;
&lt;li&gt;Never infer phone numbers or hidden profile fields from missing values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The useful output is not simply an Excel file. It is a set of typed tables whose&lt;br&gt;
relationships and limits remain visible after the export leaves Apify.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_pandas_pipeline_bottom" rel="noopener noreferrer"&gt;Create a bounded Zalo Dataset for pandas&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>pandas</category>
      <category>dataengineering</category>
      <category>apify</category>
    </item>
    <item>
      <title>A Repeatable Zalo Data Export Workflow for Agencies</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 16:22:52 +0000</pubDate>
      <link>https://dev.to/datakaz/a-repeatable-zalo-data-export-workflow-for-agencies-1n9j</link>
      <guid>https://dev.to/datakaz/a-repeatable-zalo-data-export-workflow-for-agencies-1n9j</guid>
      <description>&lt;p&gt;An agency handoff fails when the client receives a spreadsheet with no source,&lt;br&gt;
no limits, and no way to reproduce it. Zalo work adds another constraint: the&lt;br&gt;
data belongs to the account the client connects, and the first connection needs&lt;br&gt;
a QR approval.&lt;/p&gt;

&lt;p&gt;The workflow below turns that constraint into a clear operating procedure. It&lt;br&gt;
is built for agencies and automation teams supporting sales, CRM, community,&lt;br&gt;
or data operations in Vietnam.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7RLJMDQBHqo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I built the Zalo Data Exporter used in this workflow. I may&lt;br&gt;
earn revenue from Apify when the Actor is run through links in this article,&lt;br&gt;
at no extra cost to you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Start with the deliverable
&lt;/h2&gt;

&lt;p&gt;Do not begin by asking for every available Zalo record. Pick the file the client&lt;br&gt;
will use.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client job&lt;/th&gt;
&lt;th&gt;Actor preset&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reconcile a joined-group roster&lt;/td&gt;
&lt;td&gt;&lt;code&gt;groupRoster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Groups, visible members, linked profiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prepare visible contacts for CRM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contactsAndProfiles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Profile and relationship rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preserve accessible business chats&lt;/td&gt;
&lt;td&gt;&lt;code&gt;conversationBackup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Conversation summaries and message rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build a bounded internal snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;completeSnapshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All visible record families&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a new engagement, &lt;code&gt;groupRoster&lt;/code&gt; is usually the easiest proof. One group and&lt;br&gt;
100 visible members produce a result the client can check without launching a&lt;br&gt;
large account-wide job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_agency_workflow_top" rel="noopener noreferrer"&gt;Open the Zalo Data Exporter&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Assign one alias to one Zalo account
&lt;/h2&gt;

&lt;p&gt;The Actor saves an encrypted login under &lt;code&gt;accountAlias&lt;/code&gt;. The alias is scoped to&lt;br&gt;
the Apify user, but it still needs a simple naming rule inside an agency account.&lt;/p&gt;

&lt;p&gt;Use names that identify the operational connection without exposing a phone&lt;br&gt;
number or secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accountAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"client-acme-vn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exportMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"groupRoster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxGroups"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxMembersPerGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a second client connects another Zalo account, give it another alias. Do not&lt;br&gt;
reuse an alias to switch accounts. Refresh the same alias only when the same&lt;br&gt;
connected account needs a new login.&lt;/p&gt;
&lt;h2&gt;
  
  
  Put the QR step in the onboarding call
&lt;/h2&gt;

&lt;p&gt;The first connection is not an unattended background task. Schedule it as part&lt;br&gt;
of onboarding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the small starter run in Apify Console.&lt;/li&gt;
&lt;li&gt;Ask the account owner to scan the temporary QR in Zalo.&lt;/li&gt;
&lt;li&gt;Keep the run open until the Dataset appears.&lt;/li&gt;
&lt;li&gt;Confirm the account alias and the returned record types.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The QR expires quickly, so the account owner should be ready before the run&lt;br&gt;
starts. The QR record is removed after success, rejection, or expiry. Later&lt;br&gt;
runs can reuse the encrypted saved login while Zalo accepts the session.&lt;/p&gt;
&lt;h2&gt;
  
  
  Define limits in the statement of work
&lt;/h2&gt;

&lt;p&gt;Record limits are part of the deliverable, not hidden technical settings. Write&lt;br&gt;
them into the project scope.&lt;/p&gt;

&lt;p&gt;Example group-roster scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exportMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"groupRoster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accountAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"client-acme-vn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxGroups"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxMembersPerGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example chat scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exportMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"conversationBackup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accountAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"client-acme-vn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messagesSince"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConversations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxMessagesPerConversation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeMessageText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeAttachmentMetadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The limits control runtime, output volume, and the possible Actor charge. They&lt;br&gt;
also make acceptance testing possible. A client can verify three requested&lt;br&gt;
conversations more easily than an undefined request for the entire account.&lt;/p&gt;
&lt;h2&gt;
  
  
  Deliver typed views, not one flattened mystery file
&lt;/h2&gt;

&lt;p&gt;The Dataset contains five possible &lt;code&gt;recordType&lt;/code&gt; values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;profile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;group&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;groupMember&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conversation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apify provides focused views for each family. Keep &lt;code&gt;recordType&lt;/code&gt; in raw CSV or&lt;br&gt;
JSON exports so the client can reconstruct the source model.&lt;/p&gt;

&lt;p&gt;For spreadsheet delivery, a practical package is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delivery/
  groups.csv
  group-members.csv
  profiles.csv
  run-summary.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run summary should state the account alias, collection time, configured&lt;br&gt;
limits, returned counts, and any partial conversation status. Never turn a&lt;br&gt;
partial read into a silent success claim.&lt;/p&gt;
&lt;h2&gt;
  
  
  Connect the Dataset to the client stack
&lt;/h2&gt;

&lt;p&gt;Developers can retrieve the result with &lt;code&gt;ApifyClient&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;


&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kazkn/zalo-member-profile-exporter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exportMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupRoster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client-acme-vn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxGroups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxMembersPerGroup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;list_items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, send group-member rows to a CRM staging table, save profiles to a&lt;br&gt;
warehouse, or publish a controlled spreadsheet. The client remains responsible&lt;br&gt;
for deciding which exported fields belong in each downstream system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set support expectations before the first run
&lt;/h2&gt;

&lt;p&gt;Three cases deserve explicit wording in the handoff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new or expired session needs another QR approval.&lt;/li&gt;
&lt;li&gt;The Actor returns only data visible to the connected account.&lt;/li&gt;
&lt;li&gt;Zalo can change its private web behavior, challenge a session, or stop a
history read at a platform boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Actor does not recover deleted messages, discover hidden members, join&lt;br&gt;
groups, or send messages. An agency should reject those requirements instead of&lt;br&gt;
burying them in a generic scraping promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agency delivery checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One saved alias per connected Zalo account.&lt;/li&gt;
&lt;li&gt;One small proof run before increasing limits.&lt;/li&gt;
&lt;li&gt;A named preset and explicit record cap in the project scope.&lt;/li&gt;
&lt;li&gt;Separate Dataset views plus the run summary.&lt;/li&gt;
&lt;li&gt;A documented QR refresh contact on the client side.&lt;/li&gt;
&lt;li&gt;No Apify token, QR image, or session material in the delivery folder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup gives the client a reproducible export and gives the agency a support&lt;br&gt;
boundary it can explain before the first invoice.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_agency_workflow_bottom" rel="noopener noreferrer"&gt;Run the agency starter export on Apify&lt;/a&gt;&lt;/p&gt;

</description>
      <category>apify</category>
      <category>automation</category>
      <category>agencies</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>How to Run a Zalo Data Export from Python with the Apify API</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 16:21:23 +0000</pubDate>
      <link>https://dev.to/datakaz/how-to-run-a-zalo-data-export-from-python-with-the-apify-api-238g</link>
      <guid>https://dev.to/datakaz/how-to-run-a-zalo-data-export-from-python-with-the-apify-api-238g</guid>
      <description>&lt;p&gt;A useful Zalo export should end as records your code can process. For a group&lt;br&gt;
roster, that means separate group, member, and profile rows with stable IDs. For&lt;br&gt;
a chat backup, it means conversation summaries and message rows with explicit&lt;br&gt;
limits.&lt;/p&gt;

&lt;p&gt;This guide runs a bounded export with Python, retrieves the Apify Dataset, and&lt;br&gt;
splits the result by &lt;code&gt;recordType&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7RLJMDQBHqo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I built the Zalo Data Exporter used below. I may earn revenue&lt;br&gt;
from Apify when the Actor is run through links in this article, at no extra&lt;br&gt;
cost to you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The first connection is interactive
&lt;/h2&gt;

&lt;p&gt;The Actor connects to your own Zalo account. It does not scrape anonymous or&lt;br&gt;
hidden account data.&lt;/p&gt;

&lt;p&gt;Your first run for an account alias requires a temporary QR:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start a small run in the Apify Console.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;Output&lt;/code&gt; and select &lt;code&gt;First run: scan this Zalo QR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Scan and approve the QR in the Zalo mobile app.&lt;/li&gt;
&lt;li&gt;Wait for the Dataset to appear.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The QR expires after roughly 100 seconds. Once the connection succeeds, later&lt;br&gt;
runs under the same Apify user and &lt;code&gt;accountAlias&lt;/code&gt; can reuse the encrypted saved&lt;br&gt;
login while it remains valid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_python_api_top" rel="noopener noreferrer"&gt;Open the Zalo Data Exporter&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Install the Python client
&lt;/h2&gt;

&lt;p&gt;Create a virtual environment if your project does not already have one, then&lt;br&gt;
install the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;apify-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load the Apify token from an environment variable. Do not put it in the script&lt;br&gt;
or paste it into a public prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-token-from-apify-console"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run a bounded group roster export
&lt;/h2&gt;

&lt;p&gt;The starter input asks for one joined group and at most 100 visible members.&lt;br&gt;
Those limits keep the first automated call easy to inspect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;


&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;run_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exportMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupRoster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxGroups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxMembersPerGroup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kazkn/zalo-member-profile-exporter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;list_items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; records&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Actor writes a normalized Dataset. A single run can contain several record&lt;br&gt;
families, so keep &lt;code&gt;recordType&lt;/code&gt; when you store or transform the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;


&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A group roster can return &lt;code&gt;group&lt;/code&gt;, &lt;code&gt;groupMember&lt;/code&gt;, and &lt;code&gt;profile&lt;/code&gt; records. A&lt;br&gt;
conversation backup can return &lt;code&gt;conversation&lt;/code&gt; and &lt;code&gt;message&lt;/code&gt; records.&lt;/p&gt;
&lt;h2&gt;
  
  
  Select the records your job needs
&lt;/h2&gt;

&lt;p&gt;Filtering locally is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupMember&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;member_rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;profiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;member_rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;displayName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;displayName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zaloName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zaloName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avatarUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avatarUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Profile fields are nullable because Zalo does not expose every field for every&lt;br&gt;
visible member. Keep the member row even when its linked profile is incomplete.&lt;/p&gt;
&lt;h2&gt;
  
  
  Switch to another export preset
&lt;/h2&gt;

&lt;p&gt;The same API call supports four outcomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;&lt;code&gt;exportMode&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Main record types&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Joined-group roster&lt;/td&gt;
&lt;td&gt;&lt;code&gt;groupRoster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;group&lt;/code&gt;, &lt;code&gt;groupMember&lt;/code&gt;, &lt;code&gt;profile&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visible contacts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contactsAndProfiles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;profile&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessible chat backup&lt;/td&gt;
&lt;td&gt;&lt;code&gt;conversationBackup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;conversation&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visible-account snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;completeSnapshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All five record types&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A bounded conversation export looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;run_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exportMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversationBackup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messagesSince&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxConversations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxMessagesPerConversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;includeMessageText&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;includeAttachmentMetadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conversation records report whether the read reached the start returned by&lt;br&gt;
Zalo Web, stopped at your limit, or hit another bounded platform condition. Do&lt;br&gt;
not label a partial conversation as a complete archive.&lt;/p&gt;
&lt;h2&gt;
  
  
  Handle reconnects without breaking the workflow
&lt;/h2&gt;

&lt;p&gt;If the saved login expires, the run can ask for another QR. Set&lt;br&gt;
&lt;code&gt;reconnectZalo&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; for one refresh run and approve the new QR in the&lt;br&gt;
Console. Keep the same alias when refreshing the same Zalo account.&lt;/p&gt;

&lt;p&gt;Use a different alias for a different Zalo account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client-vietnam-02&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first call for that alias will require its own QR approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start with explicit record limits.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;recordType&lt;/code&gt;, stable IDs, and completeness fields.&lt;/li&gt;
&lt;li&gt;Store the Apify token in an environment variable.&lt;/li&gt;
&lt;li&gt;Expect a manual QR step for a new or expired saved login.&lt;/li&gt;
&lt;li&gt;Use one account alias per connected Zalo account.&lt;/li&gt;
&lt;li&gt;Retry only after reading the public error code and run status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API automates the repeatable part of the workflow. Authentication remains&lt;br&gt;
attached to the account owner, and the Dataset only contains data visible to&lt;br&gt;
that connected account.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_python_api_bottom" rel="noopener noreferrer"&gt;Run a bounded Zalo export from Python&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>apify</category>
      <category>automation</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Zalo Chat Backup: Export Messages and Contacts to JSON or CSV</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 01:22:25 +0000</pubDate>
      <link>https://dev.to/datakaz/zalo-chat-backup-export-messages-and-contacts-to-json-or-csv-24na</link>
      <guid>https://dev.to/datakaz/zalo-chat-backup-export-messages-and-contacts-to-json-or-csv-24na</guid>
      <description>&lt;p&gt;A backup that only restores inside an app is useful when you change phones. It&lt;br&gt;
is much less useful when you need to search conversations, audit a date range,&lt;br&gt;
prepare contact data, or feed a Dataset into Python.&lt;/p&gt;

&lt;p&gt;For those jobs, the important question is not “Can I copy my Zalo account?” It&lt;br&gt;
is: &lt;strong&gt;Which visible records do I need, and how do I export them with explicit&lt;br&gt;
limits?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This guide covers four practical workflows: chat backup, contact export, group&lt;br&gt;
roster, and a complete visible-account snapshot.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7RLJMDQBHqo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I built the Zalo Data Exporter used in this guide. If you sign&lt;br&gt;
up or run it through links in this article, I may earn revenue from Apify at&lt;br&gt;
no additional cost to you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Choose one outcome, not every data type
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;Main Dataset views&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Back up accessible messages&lt;/td&gt;
&lt;td&gt;&lt;code&gt;conversationBackup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Conversations, Messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export visible contacts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contactsAndProfiles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Contacts and profiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export members from joined groups&lt;/td&gt;
&lt;td&gt;&lt;code&gt;groupRoster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Groups, Group members, Profiles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create one bounded visible-data snapshot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;completeSnapshot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All five record types&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Starting with one preset makes the first result faster to inspect and cheaper to&lt;br&gt;
bound. A complete snapshot is useful only when you genuinely need all record&lt;br&gt;
families together.&lt;/p&gt;
&lt;h2&gt;
  
  
  How authentication works
&lt;/h2&gt;

&lt;p&gt;The first run generates a temporary QR in that run's Output. You scan and&lt;br&gt;
approve it with the Zalo mobile app. After successful authentication, the Actor&lt;br&gt;
stores an encrypted session separated by Apify user and &lt;code&gt;accountAlias&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Later runs can reconnect automatically. A QR may still be required after&lt;br&gt;
expiry, revocation, or a Zalo challenge. This is why “one QR forever” would be&lt;br&gt;
the wrong promise.&lt;/p&gt;
&lt;h2&gt;
  
  
  Workflow 1: back up accessible conversations
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_chat_backup_top" rel="noopener noreferrer"&gt;Open Zalo Data Exporter&lt;/a&gt;&lt;br&gt;
and choose &lt;strong&gt;💬 Conversation backup&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Use a date and message limit that match the question you are trying to answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exportMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"conversationBackup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accountAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messagesSince"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConversations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxMessagesPerConversation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeMessageText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeAttachmentMetadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unified Dataset returns &lt;code&gt;conversation&lt;/code&gt; and &lt;code&gt;message&lt;/code&gt; rows. Each conversation&lt;br&gt;
includes a completeness status:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;COMPLETE_TO_ZALO_WEB_START&lt;/code&gt; — Zalo returned the real start available on Zalo Web;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PARTIAL_LIMIT&lt;/code&gt; — your date or message limit stopped the read;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PARTIAL_ZALO_BOUNDARY&lt;/code&gt; — Zalo or a safety boundary stopped it first;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FAILED&lt;/code&gt; — no safe history result was returned for that conversation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction matters. “The run succeeded” and “every conversation is&lt;br&gt;
complete” are not the same statement.&lt;/p&gt;
&lt;h2&gt;
  
  
  Workflow 2: export contacts for a spreadsheet or CRM
&lt;/h2&gt;

&lt;p&gt;Choose &lt;strong&gt;👤 Contacts &amp;amp; profiles&lt;/strong&gt;. The profile rows can include Zalo user ID,&lt;br&gt;
display name, Zalo name, avatar URL, and relationship when those fields are&lt;br&gt;
visible.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;userId&lt;/code&gt; or &lt;code&gt;recordKey&lt;/code&gt; as the stable key. Do not treat display names as&lt;br&gt;
unique, and do not invent phone numbers or emails when Zalo leaves a field&lt;br&gt;
empty.&lt;/p&gt;
&lt;h2&gt;
  
  
  Workflow 3: export joined-group members
&lt;/h2&gt;

&lt;p&gt;Choose &lt;strong&gt;👥 Group roster&lt;/strong&gt; and start with one group and 100 members. The video&lt;br&gt;
at the top demonstrates this workflow from QR to spreadsheet.&lt;/p&gt;

&lt;p&gt;The Actor separates group, membership, and profile records so one person can&lt;br&gt;
belong to multiple groups without corrupting your contact table.&lt;/p&gt;
&lt;h2&gt;
  
  
  Workflow 4: create a complete visible-account snapshot
&lt;/h2&gt;

&lt;p&gt;Choose &lt;strong&gt;📦 Complete visible-account snapshot&lt;/strong&gt; only when your use case needs&lt;br&gt;
contacts, profiles, groups, members, conversations, and messages together.&lt;/p&gt;

&lt;p&gt;Set all limits explicitly. More record families mean more runtime, more Dataset&lt;br&gt;
rows, and a higher possible charge.&lt;/p&gt;
&lt;h2&gt;
  
  
  Automate a bounded chat export with Python
&lt;/h2&gt;

&lt;p&gt;Complete the first QR connection in the Apify Console, then call the saved&lt;br&gt;
session from code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kazkn/zalo-member-profile-exporter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exportMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversationBackup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxConversations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxMessagesPerConversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;includeMessageText&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;list_items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;
&lt;span class="n"&gt;conversations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conversations&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;APIFY_API_TOKEN&lt;/code&gt; in an environment variable. Never paste it into the&lt;br&gt;
article, source code, or an AI assistant.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSV, Excel, or JSON?
&lt;/h2&gt;

&lt;p&gt;Choose based on the next step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSV&lt;/strong&gt; for imports and simple pipelines;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excel&lt;/strong&gt; for filtering, manual review, and sharing with spreadsheet users;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON&lt;/strong&gt; for APIs, nested metadata, and code;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset API&lt;/strong&gt; for scheduled or event-driven workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Output tab exposes focused views, while API users can filter the unified&lt;br&gt;
Dataset on &lt;code&gt;recordType&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy and product boundaries
&lt;/h2&gt;

&lt;p&gt;Zalo Data Exporter is read-only. It has no path for sending messages, adding&lt;br&gt;
friends, or changing group membership. It does not bypass authentication,&lt;br&gt;
recover deleted messages, discover unjoined groups, or access mobile-only&lt;br&gt;
encrypted archives.&lt;/p&gt;

&lt;p&gt;Zalo can change private behavior, invalidate sessions, or require another&lt;br&gt;
verification. Start with a non-critical account, conservative limits, and an&lt;br&gt;
appropriate retention policy for exported membership and message data.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I back up all Zalo messages?
&lt;/h3&gt;

&lt;p&gt;The Actor can export accessible direct and joined-group history within your&lt;br&gt;
date, message, time, and platform boundaries. It does not promise universal or&lt;br&gt;
deleted-message recovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can later runs run without a QR?
&lt;/h3&gt;

&lt;p&gt;They can reuse the encrypted session for the same Apify user and&lt;br&gt;
&lt;code&gt;accountAlias&lt;/code&gt;. A new QR is required when the saved login expires or is revoked.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when a QR expires?
&lt;/h3&gt;

&lt;p&gt;The run reports &lt;code&gt;QR_EXPIRED&lt;/code&gt;. Start a new run and scan the new code; the old QR&lt;br&gt;
is intentionally deleted and cannot be reused.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is the Actor priced?
&lt;/h3&gt;

&lt;p&gt;It uses pay-per-event pricing for the run start and delivered record types.&lt;br&gt;
Prices vary with the Apify subscription. The Console shows the maximum possible&lt;br&gt;
charge before launch, so explicit small limits are the safest first step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the smallest useful export
&lt;/h2&gt;

&lt;p&gt;Choose one preset, keep the starter limits, inspect the Dataset views, and only&lt;br&gt;
then increase scope.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_chat_backup_bottom" rel="noopener noreferrer"&gt;Run a bounded Zalo export on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Apify links in this article support the Actor's developer. Results depend on&lt;br&gt;
what the connected account can see and the limits you configure.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>python</category>
      <category>apify</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Export Zalo Group Members to CSV or Excel</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 01:21:18 +0000</pubDate>
      <link>https://dev.to/datakaz/how-to-export-zalo-group-members-to-csv-or-excel-3m9m</link>
      <guid>https://dev.to/datakaz/how-to-export-zalo-group-members-to-csv-or-excel-3m9m</guid>
      <description>&lt;p&gt;Copying a Zalo group roster by hand looks manageable until you have more than&lt;br&gt;
one group. Then the workflow becomes scrolling, opening profiles, copying names,&lt;br&gt;
removing duplicates, and trying to remember which row came from which group.&lt;/p&gt;

&lt;p&gt;This guide shows a reproducible way to export the members your own Zalo account&lt;br&gt;
can see in joined groups. The result is a structured Dataset you can download as&lt;br&gt;
CSV, Excel, or JSON.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7RLJMDQBHqo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I built the Zalo Data Exporter used below. If you sign up or&lt;br&gt;
run the Actor through links in this article, I may earn revenue from Apify at&lt;br&gt;
no additional cost to you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the export does — and does not do
&lt;/h2&gt;

&lt;p&gt;The Actor authenticates your own account with a temporary Zalo QR. It is a&lt;br&gt;
read-only exporter, not an anonymous people finder.&lt;/p&gt;

&lt;p&gt;It can return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;groups the connected account has joined;&lt;/li&gt;
&lt;li&gt;members visible inside those groups;&lt;/li&gt;
&lt;li&gt;linked profile fields Zalo exposes to that account;&lt;/li&gt;
&lt;li&gt;separate Dataset views for groups, members, and profiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not discover hidden groups, invisible members, deleted messages, or a&lt;br&gt;
public phone-number database.&lt;/p&gt;
&lt;h2&gt;
  
  
  The data model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record type&lt;/th&gt;
&lt;th&gt;Useful fields&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;groupId&lt;/code&gt;, name, visible member count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;groupMember&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;groupId&lt;/code&gt;, &lt;code&gt;userId&lt;/code&gt;, display name, stable record key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zalo user ID, display name, Zalo name, avatar URL, relationship&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Keeping membership and profile rows separate matters. A person can belong to&lt;br&gt;
multiple groups without requiring duplicate profile objects.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step-by-step: export one group first
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Open the Actor
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_group_members_top" rel="noopener noreferrer"&gt;Open Zalo Data Exporter on Apify&lt;/a&gt;&lt;br&gt;
and keep the &lt;strong&gt;👥 Group roster&lt;/strong&gt; preset.&lt;/p&gt;

&lt;p&gt;The public input uses outcome presets instead of low-level scopes. You do not&lt;br&gt;
need to provide Zalo cookies, an IMEI, a proxy, or a session passphrase.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Use conservative starter limits
&lt;/h3&gt;

&lt;p&gt;Start with one group and 100 members:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exportMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"groupRoster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accountAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxGroups"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxMembersPerGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is large enough to verify the output while keeping runtime and maximum&lt;br&gt;
charge easy to understand. Increase only the limit that your use case needs.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Scan the temporary QR
&lt;/h3&gt;

&lt;p&gt;On the first connection:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Start&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Open the run's &lt;strong&gt;Output&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Open &lt;strong&gt;① First run: scan this Zalo QR&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Scan and approve it in the Zalo mobile app.&lt;/li&gt;
&lt;li&gt;Keep the run open until the Dataset appears.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The QR expires quickly. If the run reports &lt;code&gt;QR_EXPIRED&lt;/code&gt;, start a new run and&lt;br&gt;
scan the newly generated code. Later runs can reuse the encrypted session saved&lt;br&gt;
under the same &lt;code&gt;accountAlias&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Inspect focused views
&lt;/h3&gt;

&lt;p&gt;Check &lt;strong&gt;Groups&lt;/strong&gt; first to confirm the source. Then open &lt;strong&gt;Group members&lt;/strong&gt; and,&lt;br&gt;
when needed, &lt;strong&gt;Contacts and profiles&lt;/strong&gt;. Focused views make the spreadsheet easier&lt;br&gt;
to validate before download.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Export the result
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;CSV is convenient for database and CRM imports.&lt;/li&gt;
&lt;li&gt;Excel is useful for filtering and manual review.&lt;/li&gt;
&lt;li&gt;JSON preserves the record structure for code and APIs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Run the same export with Python
&lt;/h2&gt;

&lt;p&gt;Complete the interactive QR connection once in the Apify Console. Then reuse&lt;br&gt;
the saved session programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kazkn/zalo-member-profile-exporter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exportMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupRoster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxGroups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxMembersPerGroup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;list_items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;
&lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupMember&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exported &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; visible group-member rows&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load the token from &lt;code&gt;APIFY_API_TOKEN&lt;/code&gt;; never paste it into source code or an AI&lt;br&gt;
prompt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Export a specific joined group
&lt;/h2&gt;

&lt;p&gt;If you need one group, make a small discovery run first and copy its ID from the&lt;br&gt;
&lt;strong&gt;Groups&lt;/strong&gt; view. Then provide it in the advanced &lt;code&gt;groupIds&lt;/code&gt; field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exportMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"groupRoster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"groupIds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"joined-group-id-from-your-dataset"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxMembersPerGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only use IDs returned for the connected account. An inaccessible or incorrect&lt;br&gt;
ID cannot produce a roster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing and limits
&lt;/h2&gt;

&lt;p&gt;Zalo Data Exporter uses pay-per-event pricing. The start event and each saved&lt;br&gt;
profile, group, group-member, conversation, or message record have separate&lt;br&gt;
prices. Apify displays the maximum possible charge before the run.&lt;/p&gt;

&lt;p&gt;The exact cost depends on your Apify subscription and the visible records&lt;br&gt;
delivered. Small starter limits are useful for both validation and cost control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The QR expired
&lt;/h3&gt;

&lt;p&gt;Start a new run and scan the new QR immediately. An expired image cannot be&lt;br&gt;
reused.&lt;/p&gt;

&lt;h3&gt;
  
  
  The saved login no longer works
&lt;/h3&gt;

&lt;p&gt;Enable &lt;strong&gt;Refresh this saved login&lt;/strong&gt; for that same account and scan a new QR. Use&lt;br&gt;
a new &lt;code&gt;accountAlias&lt;/code&gt; for a different Zalo account.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Dataset is smaller than the group UI
&lt;/h3&gt;

&lt;p&gt;The exporter can only save members Zalo exposes to the connected account and&lt;br&gt;
within your run limits. Treat the Dataset as visible-account data, not a claim&lt;br&gt;
of universal group completeness.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I export Zalo group members without coding?
&lt;/h3&gt;

&lt;p&gt;Yes. Choose &lt;strong&gt;Group roster&lt;/strong&gt;, scan the first-run QR, and download the focused&lt;br&gt;
Dataset view as CSV or Excel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to scan a QR every time?
&lt;/h3&gt;

&lt;p&gt;Not normally. The first connection is interactive; later runs can reuse the&lt;br&gt;
encrypted saved session until Zalo invalidates or challenges it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the export include phone numbers?
&lt;/h3&gt;

&lt;p&gt;Only fields Zalo makes visible to the connected account can be returned. The&lt;br&gt;
Actor does not provide or infer a phone-number database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the bounded workflow
&lt;/h2&gt;

&lt;p&gt;Watch the six-minute video above, then repeat it with one group and 100 members.&lt;br&gt;
Validate the three focused views before increasing the limits.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=en_group_members_bottom" rel="noopener noreferrer"&gt;Export visible Zalo group members on Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Apify links in this article support the Actor's developer. Results depend on&lt;br&gt;
the connected account's visibility and configured limits.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>apify</category>
      <category>tutorial</category>
      <category>automation</category>
    </item>
    <item>
      <title>Cách xuất danh bạ Zalo ra Excel để làm sạch dữ liệu CRM</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 01:20:03 +0000</pubDate>
      <link>https://dev.to/datakaz/cach-xuat-danh-ba-zalo-ra-excel-de-lam-sach-du-lieu-crm-37l5</link>
      <guid>https://dev.to/datakaz/cach-xuat-danh-ba-zalo-ra-excel-de-lam-sach-du-lieu-crm-37l5</guid>
      <description>&lt;p&gt;Danh bạ Zalo hữu ích khi bạn đang ở trong ứng dụng. Nhưng khi cần kiểm tra bản&lt;br&gt;
ghi trùng, chuẩn hóa tên, lưu một snapshot hoặc chuẩn bị dữ liệu cho CRM, bạn&lt;br&gt;
cần một bảng có cột rõ ràng — không phải hàng trăm màn hình hồ sơ mở thủ công.&lt;/p&gt;

&lt;p&gt;Bài này hướng dẫn cách xuất &lt;strong&gt;danh bạ và hồ sơ mà tài khoản Zalo của bạn nhìn&lt;br&gt;
thấy&lt;/strong&gt; thành CSV, Excel hoặc JSON, rồi làm sạch dữ liệu mà không biến trường&lt;br&gt;
trống thành thông tin tưởng tượng.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7RLJMDQBHqo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Minh bạch:&lt;/strong&gt; Tôi là nhà phát triển của Zalo Data Exporter. Nếu bạn đăng ký&lt;br&gt;
hoặc chạy Actor qua liên kết trong bài, tôi có thể nhận doanh thu từ Apify mà&lt;br&gt;
bạn không phải trả thêm.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Danh bạ trong ứng dụng chưa phải là dữ liệu CRM
&lt;/h2&gt;

&lt;p&gt;CRM cần các bản ghi ổn định, có nguồn và có thể cập nhật. Một bảng xuất Zalo hữu&lt;br&gt;
ích nên giữ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ID người dùng Zalo;&lt;/li&gt;
&lt;li&gt;tên hiển thị và tên Zalo khi có;&lt;/li&gt;
&lt;li&gt;avatar URL khi hiển thị;&lt;/li&gt;
&lt;li&gt;trạng thái quan hệ hoặc nguồn bản ghi;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;recordKey&lt;/code&gt; để nhận biết bản ghi giữa các lần chạy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Điều quan trọng là chấp nhận trường trống. Nếu Zalo không hiển thị số điện&lt;br&gt;
thoại, email hoặc tên thật, bản xuất không nên tự đoán.&lt;/p&gt;
&lt;h2&gt;
  
  
  Chọn đúng workflow
&lt;/h2&gt;

&lt;p&gt;Zalo Data Exporter có bốn preset. Với danh bạ, hãy chọn &lt;strong&gt;👤 Contacts &amp;amp;&lt;br&gt;
profiles&lt;/strong&gt;. Không cần bật lịch sử chat hoặc danh sách nhóm nếu bạn chỉ muốn một&lt;br&gt;
snapshot liên hệ.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=vi_contacts_top" rel="noopener noreferrer"&gt;Mở preset Contacts &amp;amp; profiles&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Hướng dẫn từng bước
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Cấu hình lần chạy đầu tiên
&lt;/h3&gt;

&lt;p&gt;Input tối thiểu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exportMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contactsAndProfiles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accountAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preset quyết định loại dữ liệu. &lt;code&gt;accountAlias&lt;/code&gt; là tên phiên đăng nhập được mã&lt;br&gt;
hóa. Nếu bạn kết nối một tài khoản Zalo khác, hãy dùng alias khác.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Kết nối bằng QR tạm thời
&lt;/h3&gt;

&lt;p&gt;Bấm &lt;strong&gt;Start&lt;/strong&gt;, mở &lt;strong&gt;Output&lt;/strong&gt;, quét QR trong ứng dụng Zalo và phê duyệt. QR sẽ bị&lt;br&gt;
xóa sau khi thành công, bị từ chối hoặc hết hạn. Những lần chạy sau có thể kết&lt;br&gt;
nối lại tự động bằng phiên đã lưu.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Mở đúng Dataset view
&lt;/h3&gt;

&lt;p&gt;Chọn &lt;strong&gt;Contacts and profiles&lt;/strong&gt;. View này tập trung các trường hữu ích cho bảng&lt;br&gt;
tính; Dataset thống nhất vẫn dùng &lt;code&gt;recordType: "profile"&lt;/code&gt; để API có thể lọc.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Tải file
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Chọn &lt;strong&gt;Excel&lt;/strong&gt; nếu người xử lý tiếp theo làm việc trong bảng tính.&lt;/li&gt;
&lt;li&gt;Chọn &lt;strong&gt;CSV&lt;/strong&gt; cho import CRM hoặc xử lý hàng loạt.&lt;/li&gt;
&lt;li&gt;Chọn &lt;strong&gt;JSON&lt;/strong&gt; để giữ kiểu dữ liệu cho code và API.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Làm sạch bảng trước khi nhập CRM
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Giữ khóa, không chỉ giữ tên
&lt;/h3&gt;

&lt;p&gt;Hai người có thể có cùng tên hiển thị. Dùng &lt;code&gt;userId&lt;/code&gt; hoặc &lt;code&gt;recordKey&lt;/code&gt; làm khóa&lt;br&gt;
chính; dùng tên để hiển thị.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ghi nguồn của snapshot
&lt;/h3&gt;

&lt;p&gt;Thêm các cột nội bộ như &lt;code&gt;source = Zalo&lt;/code&gt;, &lt;code&gt;exportedAt&lt;/code&gt;, và &lt;code&gt;accountAlias&lt;/code&gt;. Khi dữ&lt;br&gt;
liệu thay đổi, bạn sẽ biết lần xuất nào tạo ra bản ghi.&lt;/p&gt;
&lt;h3&gt;
  
  
  Không điền giả trường thiếu
&lt;/h3&gt;

&lt;p&gt;Nếu số điện thoại không xuất hiện, giữ trống. Một CRM sạch với trường thiếu tốt&lt;br&gt;
hơn một CRM đầy dữ liệu không kiểm chứng.&lt;/p&gt;
&lt;h3&gt;
  
  
  Xử lý người xuất hiện ở nhiều nguồn
&lt;/h3&gt;

&lt;p&gt;Một người có thể nằm trong danh bạ và trong nhiều nhóm. Nếu bạn kết hợp preset&lt;br&gt;
Contacts với Group roster, hãy tách bảng quan hệ nhóm khỏi bảng hồ sơ thay vì&lt;br&gt;
xóa mọi dòng “trùng tên”.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tự động hóa snapshot bằng Python
&lt;/h2&gt;

&lt;p&gt;Sau lần xác thực QR đầu tiên:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kazkn/zalo-member-profile-exporter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exportMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contactsAndProfiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;list_items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;
&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Snapshot contains &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; visible profiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bạn có thể chạy định kỳ trên Apify, gửi Dataset qua webhook hoặc tải CSV vào&lt;br&gt;
quy trình hiện có. Hãy đặt giới hạn và lịch chạy phù hợp thay vì thu thập lại&lt;br&gt;
liên tục mà không có mục tiêu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Khi nào nên dùng Complete snapshot?
&lt;/h2&gt;

&lt;p&gt;Chỉ dùng &lt;strong&gt;📦 Complete visible-account snapshot&lt;/strong&gt; nếu bạn thật sự cần danh bạ,&lt;br&gt;
hồ sơ, nhóm, thành viên, cuộc trò chuyện và tin nhắn trong cùng một run. Nó tạo&lt;br&gt;
nhiều loại bản ghi hơn, chạy lâu hơn và có thể tốn nhiều hơn.&lt;/p&gt;

&lt;p&gt;Đối với một dự án CRM chỉ cần danh bạ, preset chuyên biệt đơn giản và dễ kiểm&lt;br&gt;
tra hơn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giới hạn và quyền riêng tư
&lt;/h2&gt;

&lt;p&gt;Actor chỉ đọc dữ liệu hiển thị cho tài khoản đã kết nối. Nó không tìm hồ sơ ẩn,&lt;br&gt;
nhóm chưa tham gia, cơ sở dữ liệu số điện thoại hay tin nhắn đã xóa. Nó cũng&lt;br&gt;
không gửi tin nhắn, kết bạn hoặc thay đổi nhóm.&lt;/p&gt;

&lt;p&gt;Áp dụng thời hạn lưu trữ phù hợp, hạn chế quyền truy cập file xuất và chỉ giữ&lt;br&gt;
những trường cần thiết cho mục tiêu của bạn.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Có thể xuất danh bạ Zalo ra CSV không?
&lt;/h3&gt;

&lt;p&gt;Có. Chọn &lt;strong&gt;Contacts &amp;amp; profiles&lt;/strong&gt;, kết nối QR lần đầu, rồi tải view tương ứng dưới&lt;br&gt;
dạng CSV.&lt;/p&gt;

&lt;h3&gt;
  
  
  File có luôn chứa số điện thoại không?
&lt;/h3&gt;

&lt;p&gt;Không. Trường chỉ có giá trị khi Zalo hiển thị cho tài khoản. Công cụ không suy&lt;br&gt;
đoán hoặc mua dữ liệu số điện thoại.&lt;/p&gt;

&lt;h3&gt;
  
  
  Có thể dùng file trong CRM không?
&lt;/h3&gt;

&lt;p&gt;Có, sau khi bạn chọn khóa ổn định, chuẩn hóa cột và giữ trường thiếu ở trạng&lt;br&gt;
thái trống. CSV là định dạng import phổ biến; JSON phù hợp hơn cho API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tạo snapshot danh bạ đầu tiên
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=vi_contacts_bottom" rel="noopener noreferrer"&gt;Xuất danh bạ Zalo hiển thị ra CSV, Excel hoặc JSON&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Liên kết Apify trong bài hỗ trợ nhà phát triển của Actor. Kết quả phụ thuộc vào&lt;br&gt;
dữ liệu tài khoản đã kết nối có thể nhìn thấy.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>automation</category>
      <category>apify</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Cách quét thành viên nhóm Zalo và xuất danh sách ra Excel</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 01:19:47 +0000</pubDate>
      <link>https://dev.to/datakaz/cach-quet-thanh-vien-nhom-zalo-va-xuat-danh-sach-ra-excel-1180</link>
      <guid>https://dev.to/datakaz/cach-quet-thanh-vien-nhom-zalo-va-xuat-danh-sach-ra-excel-1180</guid>
      <description>&lt;p&gt;Bạn quản lý một cộng đồng Zalo và cần danh sách thành viên để đối chiếu, làm&lt;br&gt;
báo cáo hoặc chuẩn bị dữ liệu CRM. Cách thủ công là mở từng nhóm, cuộn danh&lt;br&gt;
sách, sao chép từng tên và ghép lại trong Excel. Với vài chục thành viên thì&lt;br&gt;
được. Với nhiều nhóm, quy trình này nhanh chóng trở thành một công việc lặp lại&lt;br&gt;
và dễ sai.&lt;/p&gt;

&lt;p&gt;Hướng dẫn này chỉ cách &lt;strong&gt;xuất những thành viên mà tài khoản của bạn nhìn thấy&lt;br&gt;
trong các nhóm đã tham gia&lt;/strong&gt; sang CSV, Excel hoặc JSON.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7RLJMDQBHqo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Minh bạch:&lt;/strong&gt; Tôi xây dựng Zalo Data Exporter. Nếu bạn đăng ký hoặc chạy&lt;br&gt;
Actor qua liên kết trong bài, tôi có thể nhận doanh thu từ Apify mà bạn không&lt;br&gt;
phải trả thêm.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  “Quét thành viên nhóm Zalo” ở đây có nghĩa gì?
&lt;/h2&gt;

&lt;p&gt;Đây không phải là công cụ tìm số điện thoại công khai hoặc xâm nhập nhóm ẩn.&lt;br&gt;
Actor kết nối tài khoản Zalo của chính bạn bằng QR tạm thời và đọc dữ liệu hiển&lt;br&gt;
thị cho tài khoản đó.&lt;/p&gt;

&lt;p&gt;Kết quả được tách thành ba loại:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset view&lt;/th&gt;
&lt;th&gt;Dữ liệu chính&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Groups&lt;/td&gt;
&lt;td&gt;ID nhóm, tên nhóm, số thành viên hiển thị&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Group members&lt;/td&gt;
&lt;td&gt;ID nhóm, ID người dùng, tên hiển thị, khóa bản ghi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contacts and profiles&lt;/td&gt;
&lt;td&gt;Tên Zalo, avatar và mối quan hệ khi Zalo hiển thị&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tách view như vậy giúp bạn tải đúng bảng thay vì nhận một file JSON lộn xộn.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quy trình từng bước
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Bước 1: Chọn Group roster
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=vi_group_members_top" rel="noopener noreferrer"&gt;Mở Zalo Data Exporter trên Apify&lt;/a&gt;&lt;br&gt;
và giữ preset &lt;strong&gt;👥 Group roster&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Không cần nhập cookie, token Zalo, IMEI, proxy hoặc mật khẩu phiên. Form chỉ hỏi&lt;br&gt;
kết quả và giới hạn bạn muốn.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bước 2: Giữ giới hạn lần đầu nhỏ
&lt;/h3&gt;

&lt;p&gt;Khởi đầu với:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exportMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"groupRoster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accountAlias"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxGroups"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxMembersPerGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Một nhóm và 100 thành viên đủ để kiểm tra toàn bộ workflow. Sau khi Dataset&lt;br&gt;
đúng, tăng riêng giới hạn bạn cần.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bước 3: Quét QR ngay khi run bắt đầu
&lt;/h3&gt;

&lt;p&gt;Ở lần chạy đầu tiên:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bấm &lt;strong&gt;Start&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Mở tab &lt;strong&gt;Output&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Bấm &lt;strong&gt;① First run: scan this Zalo QR&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Quét và phê duyệt trong ứng dụng Zalo.&lt;/li&gt;
&lt;li&gt;Giữ run mở cho đến khi Dataset xuất hiện.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;QR có thời hạn ngắn. Nếu bạn nhận &lt;code&gt;QR_EXPIRED&lt;/code&gt;, chạy lại và quét mã mới ngay.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bước 4: Kiểm tra ba view
&lt;/h3&gt;

&lt;p&gt;Mở &lt;strong&gt;Groups&lt;/strong&gt; trước để chắc chắn bạn đã lấy đúng nhóm. Sau đó mở &lt;strong&gt;Group&lt;br&gt;
members&lt;/strong&gt; để xem danh sách. Cuối cùng, kiểm tra &lt;strong&gt;Contacts and profiles&lt;/strong&gt; nếu&lt;br&gt;
bạn cần thêm trường hồ sơ mà Zalo hiển thị.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bước 5: Tải Excel, CSV hoặc JSON
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Excel/CSV:&lt;/strong&gt; phù hợp cho lọc, loại trùng, đối chiếu và nhập CRM;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON:&lt;/strong&gt; phù hợp cho code, API hoặc kho dữ liệu;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset API:&lt;/strong&gt; phù hợp cho workflow định kỳ.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Dùng Python cho lần chạy sau
&lt;/h2&gt;

&lt;p&gt;Sau khi đã kết nối QR một lần trong Console, bạn có thể tái sử dụng phiên đăng&lt;br&gt;
nhập đã mã hóa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kazkn/zalo-member-profile-exporter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exportMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupRoster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxGroups&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxMembersPerGroup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;list_items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;
&lt;span class="n"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;groupMember&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;members&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; visible members exported&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mỗi tài khoản Zalo khác nên dùng một &lt;code&gt;accountAlias&lt;/code&gt; khác. Không chia sẻ&lt;br&gt;
&lt;code&gt;APIFY_API_TOKEN&lt;/code&gt; và không dán token vào chatbot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Làm sạch dữ liệu trước khi đưa vào CRM
&lt;/h2&gt;

&lt;p&gt;Một danh sách thành viên không tự động trở thành lead list. Trước khi nhập CRM:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;giữ &lt;code&gt;recordKey&lt;/code&gt; hoặc cặp &lt;code&gt;groupId + userId&lt;/code&gt; làm khóa ổn định;&lt;/li&gt;
&lt;li&gt;giữ tên nhóm để biết nguồn của mỗi dòng;&lt;/li&gt;
&lt;li&gt;không suy đoán số điện thoại khi trường đó không hiển thị;&lt;/li&gt;
&lt;li&gt;loại trùng theo mục tiêu của bạn: một người có thể thuộc nhiều nhóm;&lt;/li&gt;
&lt;li&gt;áp dụng thời hạn lưu trữ phù hợp với workflow của bạn.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Những giới hạn cần biết
&lt;/h2&gt;

&lt;p&gt;Actor không đọc nhóm chưa tham gia, thành viên bị ẩn, tin nhắn đã xóa hoặc cơ&lt;br&gt;
sở dữ liệu số điện thoại. Zalo có thể yêu cầu xác minh lại, giới hạn truy cập&lt;br&gt;
hoặc thay đổi dữ liệu trả về. Vì vậy, hãy bắt đầu bằng một tài khoản không quan&lt;br&gt;
trọng và phạm vi nhỏ.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Có thể xuất toàn bộ thành viên của mọi nhóm Zalo không?
&lt;/h3&gt;

&lt;p&gt;Không nên hiểu như vậy. Actor chỉ xuất thành viên hiển thị trong những nhóm mà&lt;br&gt;
tài khoản đã kết nối có thể truy cập, trong giới hạn của run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Có thể chọn một nhóm cụ thể không?
&lt;/h3&gt;

&lt;p&gt;Có. Bạn có thể nhập ID của nhóm đã tham gia trong trường nâng cao. Cách đơn&lt;br&gt;
giản nhất là chạy một lần nhỏ, lấy ID từ view &lt;strong&gt;Groups&lt;/strong&gt;, rồi dùng ID đó ở run&lt;br&gt;
tiếp theo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Có cần cài phần mềm trên máy không?
&lt;/h3&gt;

&lt;p&gt;Không. Actor chạy trên Apify. Bạn chỉ cần trình duyệt để cấu hình và điện thoại&lt;br&gt;
để phê duyệt QR lần kết nối đầu tiên.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chạy thử với một nhóm
&lt;/h2&gt;

&lt;p&gt;Video ở đầu bài cho thấy toàn bộ quy trình từ QR đến Excel. Hãy lặp lại đúng&lt;br&gt;
workflow đó với một nhóm và tối đa 100 thành viên trước khi mở rộng.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=vi_group_members_bottom" rel="noopener noreferrer"&gt;Xuất thành viên nhóm Zalo ra Excel hoặc CSV&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Liên kết Apify trong bài hỗ trợ nhà phát triển của Actor. Dữ liệu trả về phụ&lt;br&gt;
thuộc vào quyền hiển thị của tài khoản đã kết nối.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>tutorial</category>
      <category>automation</category>
      <category>apify</category>
    </item>
    <item>
      <title>Sao lưu tin nhắn Zalo ra CSV, Excel và JSON: hướng dẫn từng bước</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Sun, 09 Aug 2026 01:18:57 +0000</pubDate>
      <link>https://dev.to/datakaz/sao-luu-tin-nhan-zalo-ra-csv-excel-va-json-huong-dan-tung-buoc-1lp5</link>
      <guid>https://dev.to/datakaz/sao-luu-tin-nhan-zalo-ra-csv-excel-va-json-huong-dan-tung-buoc-1lp5</guid>
      <description>&lt;p&gt;Bạn có thể sao lưu Zalo để đổi điện thoại. Nhưng khi cần tìm một câu trong hàng&lt;br&gt;
nghìn tin nhắn, lọc theo ngày trong Excel hoặc đưa lịch sử chat vào một script,&lt;br&gt;
bản sao lưu trong ứng dụng không phải là định dạng bạn cần.&lt;/p&gt;

&lt;p&gt;Vấn đề thật sự là: &lt;strong&gt;làm sao xuất phần lịch sử Zalo mà tài khoản của bạn nhìn&lt;br&gt;
thấy thành dữ liệu có cấu trúc?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trong hướng dẫn này, tôi sẽ chỉ cách tạo một bản xuất giới hạn, đọc được trong&lt;br&gt;
CSV, Excel hoặc JSON — đồng thời giải thích rõ những gì công cụ không thể lấy.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/7RLJMDQBHqo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Minh bạch:&lt;/strong&gt; Tôi là nhà phát triển của Zalo Data Exporter. Nếu bạn đăng ký&lt;br&gt;
hoặc chạy Actor qua liên kết trong bài, tôi có thể nhận doanh thu từ Apify mà&lt;br&gt;
bạn không phải trả thêm.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Sao lưu Zalo và xuất dữ liệu không giống nhau
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mục tiêu&lt;/th&gt;
&lt;th&gt;Sao lưu trong Zalo&lt;/th&gt;
&lt;th&gt;Xuất dữ liệu có cấu trúc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Khôi phục khi đổi điện thoại&lt;/td&gt;
&lt;td&gt;Phù hợp&lt;/td&gt;
&lt;td&gt;Không phải mục tiêu chính&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mở trong Excel&lt;/td&gt;
&lt;td&gt;Hạn chế&lt;/td&gt;
&lt;td&gt;Có&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lọc theo ngày, người gửi, cuộc trò chuyện&lt;/td&gt;
&lt;td&gt;Hạn chế&lt;/td&gt;
&lt;td&gt;Có&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dùng trong Python, BI hoặc kho dữ liệu&lt;/td&gt;
&lt;td&gt;Không&lt;/td&gt;
&lt;td&gt;Có, qua JSON/API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Khôi phục tin nhắn đã xóa&lt;/td&gt;
&lt;td&gt;Không&lt;/td&gt;
&lt;td&gt;Không&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Đọc dữ liệu tài khoản không nhìn thấy&lt;/td&gt;
&lt;td&gt;Không&lt;/td&gt;
&lt;td&gt;Không&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nếu mục tiêu chỉ là đổi máy, hãy dùng tính năng sao lưu chính thức. Nếu mục tiêu&lt;br&gt;
là phân tích hoặc lưu trữ bên ngoài Zalo, bạn cần một Dataset.&lt;/p&gt;
&lt;h2&gt;
  
  
  Một bản xuất chat hữu ích cần có gì?
&lt;/h2&gt;

&lt;p&gt;Không nên gom mọi thứ vào một cột văn bản. Một Dataset tốt tách ít nhất hai loại&lt;br&gt;
bản ghi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;conversation&lt;/code&gt;: ID, tiêu đề, loại cuộc trò chuyện, trạng thái đầy đủ và lý do dừng;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;message&lt;/code&gt;: ID, cuộc trò chuyện, người gửi, chiều gửi/nhận, thời gian, nội dung
và metadata tệp đính kèm nếu bạn bật tùy chọn đó.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nhờ vậy, bạn có thể lọc tin nhắn theo ngày, tìm từ khóa trên nhiều cuộc trò&lt;br&gt;
chuyện, đếm tin nhắn theo người gửi hoặc nhập JSON vào một pipeline.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cách xuất lịch sử chat Zalo trên Apify
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Mở Actor và chọn đúng preset
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=vi_chat_backup_top" rel="noopener noreferrer"&gt;Mở Zalo Data Exporter&lt;/a&gt;,&lt;br&gt;
sau đó chọn &lt;strong&gt;💬 Conversation backup&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Preset này chỉ yêu cầu những giới hạn người dùng hiểu được: số cuộc trò chuyện,&lt;br&gt;
số tin nhắn mỗi cuộc trò chuyện, mốc ngày, nội dung tin nhắn và metadata tệp.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Bắt đầu nhỏ
&lt;/h3&gt;

&lt;p&gt;Lần đầu, dùng khoảng:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exportMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"conversationBackup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConversations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxMessagesPerConversation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeMessageText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeAttachmentMetadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ba cuộc trò chuyện và 200 tin nhắn mỗi cuộc giúp bạn kiểm tra đúng tài khoản,&lt;br&gt;
đúng định dạng và đúng chi phí trước khi tăng giới hạn.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Quét mã QR tạm thời
&lt;/h3&gt;

&lt;p&gt;Ở lần kết nối đầu tiên, bấm &lt;strong&gt;Start&lt;/strong&gt;, mở &lt;strong&gt;Output → ① First run: scan this&lt;br&gt;
Zalo QR&lt;/strong&gt;, rồi quét bằng ứng dụng Zalo. QR chỉ tồn tại trong khoảng thời gian&lt;br&gt;
ngắn. Nếu hết hạn, chạy lại để nhận mã mới.&lt;/p&gt;

&lt;p&gt;Sau khi xác thực thành công, các lần chạy sau có thể dùng lại phiên đăng nhập đã&lt;br&gt;
được mã hóa cho cùng &lt;code&gt;accountAlias&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Đọc trạng thái trước khi tải file
&lt;/h3&gt;

&lt;p&gt;Một run thành công không có nghĩa là toàn bộ lịch sử Zalo đã được đọc. Mỗi dòng&lt;br&gt;
&lt;code&gt;conversation&lt;/code&gt; cho biết điều gì đã dừng quá trình:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;COMPLETE_TO_ZALO_WEB_START&lt;/code&gt;: Zalo Web đã trả về điểm bắt đầu thực tế;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PARTIAL_LIMIT&lt;/code&gt;: đã chạm mốc ngày hoặc giới hạn tin nhắn của bạn;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PARTIAL_ZALO_BOUNDARY&lt;/code&gt;: Zalo hoặc giới hạn an toàn đã dừng trước;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FAILED&lt;/code&gt;: cuộc trò chuyện đó không trả về kết quả an toàn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Đây là phần quan trọng nhất nếu bạn muốn một bản lưu trữ đáng tin: &lt;strong&gt;đọc trạng&lt;br&gt;
thái đầy đủ, không chỉ đếm số dòng&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Tải đúng Dataset view
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Chọn &lt;strong&gt;Conversations&lt;/strong&gt; để kiểm tra phạm vi và trạng thái.&lt;/li&gt;
&lt;li&gt;Chọn &lt;strong&gt;Messages&lt;/strong&gt; để tải nội dung tin nhắn.&lt;/li&gt;
&lt;li&gt;Tải &lt;strong&gt;CSV/Excel&lt;/strong&gt; để lọc trong bảng tính.&lt;/li&gt;
&lt;li&gt;Tải &lt;strong&gt;JSON&lt;/strong&gt; hoặc dùng Dataset API cho tự động hóa.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Gọi Actor bằng Python
&lt;/h2&gt;

&lt;p&gt;Sau lần quét QR đầu tiên trong Console, bạn có thể chạy lại bằng API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kazkn/zalo-member-profile-exporter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exportMode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversationBackup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountAlias&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxConversations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxMessagesPerConversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;list_items&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;
&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recordType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; messages exported&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Đừng đặt token trực tiếp trong code. Dùng biến môi trường&lt;br&gt;
&lt;code&gt;APIFY_API_TOKEN&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Những gì bản xuất không làm
&lt;/h2&gt;

&lt;p&gt;Actor chỉ đọc dữ liệu hiển thị cho tài khoản đã kết nối. Nó không:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;khôi phục tin nhắn đã xóa;&lt;/li&gt;
&lt;li&gt;đọc nhóm mà tài khoản chưa tham gia;&lt;/li&gt;
&lt;li&gt;lấy dữ liệu ẩn hoặc kho chỉ tồn tại trên thiết bị di động;&lt;/li&gt;
&lt;li&gt;gửi tin nhắn hoặc thay đổi thành viên;&lt;/li&gt;
&lt;li&gt;đảm bảo Zalo Web trả về toàn bộ lịch sử của mọi tài khoản.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Có thể xuất tin nhắn Zalo ra Excel không?
&lt;/h3&gt;

&lt;p&gt;Có. Chọn &lt;code&gt;Conversation backup&lt;/code&gt;, chạy với giới hạn nhỏ, mở view &lt;strong&gt;Messages&lt;/strong&gt; và&lt;br&gt;
tải CSV hoặc Excel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Có cần quét QR mỗi lần không?
&lt;/h3&gt;

&lt;p&gt;Không nhất thiết. Lần đầu cần QR tạm thời; lần sau Actor có thể dùng lại phiên&lt;br&gt;
đăng nhập đã mã hóa. Nếu phiên hết hạn hoặc bị Zalo thu hồi, bạn phải làm mới.&lt;/p&gt;

&lt;h3&gt;
  
  
  Có thể lấy tin nhắn đã xóa không?
&lt;/h3&gt;

&lt;p&gt;Không. Công cụ chỉ xuất dữ liệu Zalo Web trả về cho tài khoản đang kết nối.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bắt đầu với một bản xuất nhỏ
&lt;/h2&gt;

&lt;p&gt;Đừng chạy toàn bộ tài khoản ngay. Bắt đầu với 3 cuộc trò chuyện, kiểm tra trạng&lt;br&gt;
thái và định dạng, rồi mới tăng giới hạn phù hợp với nhu cầu.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://apify.com/kazkn/zalo-member-profile-exporter?utm_source=devto&amp;amp;utm_medium=affiliate&amp;amp;utm_campaign=zalo_data_export&amp;amp;utm_content=vi_chat_backup_bottom" rel="noopener noreferrer"&gt;Chạy Zalo Conversation Backup trên Apify&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Liên kết Apify trong bài hỗ trợ nhà phát triển của Actor. Kết quả phụ thuộc vào&lt;br&gt;
dữ liệu mà tài khoản của bạn có thể nhìn thấy và giới hạn bạn chọn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>automation</category>
      <category>data</category>
      <category>apify</category>
    </item>
    <item>
      <title>I Built an Apify Actor to Compare Luxury Handbag Resale Prices Across 4 Marketplaces</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Sun, 21 Jun 2026 01:01:25 +0000</pubDate>
      <link>https://dev.to/datakaz/i-built-an-apify-actor-to-compare-luxury-handbag-resale-prices-across-4-marketplaces-2lbj</link>
      <guid>https://dev.to/datakaz/i-built-an-apify-actor-to-compare-luxury-handbag-resale-prices-across-4-marketplaces-2lbj</guid>
      <description>&lt;p&gt;I built an Apify Actor that compares public resale listings for Hermes Birkin, Hermes Kelly, and Chanel Classic Flap bags across four marketplaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vestiaire Collective&lt;/li&gt;
&lt;li&gt;Rebag&lt;/li&gt;
&lt;li&gt;Fashionphile&lt;/li&gt;
&lt;li&gt;1stDibs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Actor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/hermes-birkin-kelly-arbitrage" rel="noopener noreferrer"&gt;https://apify.com/kazkn/hermes-birkin-kelly-arbitrage&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Short demo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/h0JEDFitqao?feature=share" rel="noopener noreferrer"&gt;https://youtube.com/shorts/h0JEDFitqao?feature=share&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal is not to build an auto-buy bot.&lt;/p&gt;

&lt;p&gt;The goal is to turn messy marketplace pages into a dataset that can be reviewed before any human decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Luxury resale comparison is messy because public listings are inconsistent.&lt;/p&gt;

&lt;p&gt;One marketplace exposes condition clearly. Another hides useful data inside the detail page. One title includes hardware. Another only says "leather". One listing is priced low because condition is weaker. Another is priced high because the seller is testing the market.&lt;/p&gt;

&lt;p&gt;Manual spreadsheet comparison breaks quickly.&lt;/p&gt;

&lt;p&gt;The Actor turns that into a repeatable workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Collect public listings.&lt;/li&gt;
&lt;li&gt;Normalize listing fields.&lt;/li&gt;
&lt;li&gt;Group comparable bags.&lt;/li&gt;
&lt;li&gt;Calculate gross spread.&lt;/li&gt;
&lt;li&gt;Estimate net ROI after cost assumptions.&lt;/li&gt;
&lt;li&gt;Output review candidates and warnings.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Input Design
&lt;/h2&gt;

&lt;p&gt;The input is designed for non-technical users first.&lt;/p&gt;

&lt;p&gt;Main sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scan scope&lt;/li&gt;
&lt;li&gt;Strict target&lt;/li&gt;
&lt;li&gt;Scan limits&lt;/li&gt;
&lt;li&gt;Deal thresholds&lt;/li&gt;
&lt;li&gt;Cost assumptions&lt;/li&gt;
&lt;li&gt;Platform fee assumptions&lt;/li&gt;
&lt;li&gt;Output and proxy settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users can run a broad scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platforms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"vestiaire"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rebag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fashionphile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1stdibs"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"models"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"birkin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kelly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"classic_flap"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targetingMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"broad_scan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxListingsPerMarketplace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPagesPerMarketplace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or they can run a strict watchlist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targetingMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"strict_target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targetModel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"birkin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targetSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targetColor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"black"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targetMaterial"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"togo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targetHardware"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimumCondition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"very_good"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The watchlist matters because many users already know the exact bag they want. They do not need every listing. They need to know whether a narrow target appears across platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalization
&lt;/h2&gt;

&lt;p&gt;The Actor normalizes fields like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;brand&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;model&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;colorBucket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;materialBucket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hardwareBucket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;conditionBucket&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;price&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;currency&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;platform&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;listingUrl&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hard part is not scraping the title.&lt;/p&gt;

&lt;p&gt;The hard part is turning different marketplace language into comparable buckets without pretending incomplete data is perfect.&lt;/p&gt;

&lt;p&gt;That is why the output includes warnings. A relaxed match is useful, but it is not the same as a strict match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spread And ROI
&lt;/h2&gt;

&lt;p&gt;There are two gates.&lt;/p&gt;

&lt;p&gt;Gross spread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grossSpreadPercent = (referenceSellPrice - buyCandidatePrice) / buyCandidatePrice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Estimated net ROI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;estimatedNetRoiPercent = estimatedNetProfit / landedBuyPrice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The landed buy price can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shipping or insurance estimate&lt;/li&gt;
&lt;li&gt;authentication or repair buffer&lt;/li&gt;
&lt;li&gt;FX and tax buffer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sell-side value deducts platform fee assumptions.&lt;/p&gt;

&lt;p&gt;This is intentionally conservative. A listing can have a large headline spread and still become unattractive once costs are added.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output Records
&lt;/h2&gt;

&lt;p&gt;The dataset includes different record types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;normalized_listing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spread_candidate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;watchlist_candidate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arbitrage_opportunity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unmatched_listing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;platform_error&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run_summary&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep these separate because a scraper should not overstate confidence.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;spread_candidate&lt;/code&gt; means "review this".&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;arbitrage_opportunity&lt;/code&gt; means stricter filters passed.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;platform_error&lt;/code&gt; means a marketplace failed and should not be silently ignored.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;run_summary&lt;/code&gt; helps you understand coverage, failures, and counts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Apify
&lt;/h2&gt;

&lt;p&gt;Apify is useful here because the workflow benefits from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scheduled runs&lt;/li&gt;
&lt;li&gt;proxy support&lt;/li&gt;
&lt;li&gt;structured input schema&lt;/li&gt;
&lt;li&gt;dataset output&lt;/li&gt;
&lt;li&gt;API access&lt;/li&gt;
&lt;li&gt;monetization support&lt;/li&gt;
&lt;li&gt;readable actor logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of workflow is not a one-off script. Users want to run it daily, change scan scope, export results, and plug the dataset into another process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This Actor does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authenticate bags&lt;/li&gt;
&lt;li&gt;guarantee resale profit&lt;/li&gt;
&lt;li&gt;scrape private sales&lt;/li&gt;
&lt;li&gt;provide investment advice&lt;/li&gt;
&lt;li&gt;prove sold prices&lt;/li&gt;
&lt;li&gt;bypass marketplace rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It compares public active listings and makes the review process cleaner.&lt;/p&gt;

&lt;p&gt;That boundary is important.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Improve Next
&lt;/h2&gt;

&lt;p&gt;Useful next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deeper detail page enrichment&lt;/li&gt;
&lt;li&gt;better sold-comps integration where legally available&lt;/li&gt;
&lt;li&gt;more marketplace-specific fee models&lt;/li&gt;
&lt;li&gt;alerting for saved watchlists&lt;/li&gt;
&lt;li&gt;CSV templates for reseller workflows&lt;/li&gt;
&lt;li&gt;historical price tracking per model/size/material group&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Actor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/kazkn/hermes-birkin-kelly-arbitrage" rel="noopener noreferrer"&gt;https://apify.com/kazkn/hermes-birkin-kelly-arbitrage&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Short demo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/shorts/h0JEDFitqao?feature=share" rel="noopener noreferrer"&gt;https://youtube.com/shorts/h0JEDFitqao?feature=share&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you build Apify Actors, the main takeaway is not the luxury niche. The reusable pattern is:&lt;/p&gt;

&lt;p&gt;public marketplace data -&amp;gt; normalized records -&amp;gt; comparable groups -&amp;gt; warnings -&amp;gt; review queue.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>webscraping</category>
      <category>automation</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Built the Wrong Scraper First. The Data Looked Clean and Was Still Useless.</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Fri, 12 Jun 2026 01:27:35 +0000</pubDate>
      <link>https://dev.to/datakaz/i-built-the-wrong-scraper-first-the-data-looked-clean-and-was-still-useless-cni</link>
      <guid>https://dev.to/datakaz/i-built-the-wrong-scraper-first-the-data-looked-clean-and-was-still-useless-cni</guid>
      <description>&lt;p&gt;I built the wrong scraper first.&lt;/p&gt;

&lt;p&gt;It worked.&lt;/p&gt;

&lt;p&gt;That was the problem.&lt;/p&gt;

&lt;p&gt;The scraper extracted product titles, prices, URLs, images, and brands. The dataset was clean. The rows looked normal. Nothing crashed.&lt;/p&gt;

&lt;p&gt;But the output was still not useful enough.&lt;/p&gt;

&lt;p&gt;The mistake was simple: I treated a resale marketplace like a product catalog.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;A marketplace has state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clean dataset trap
&lt;/h2&gt;

&lt;p&gt;This looked fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Leather bag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/item/123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the real questions were missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is it live or sold?&lt;/li&gt;
&lt;li&gt;Did it disappear since the last run?&lt;/li&gt;
&lt;li&gt;Which country was searched?&lt;/li&gt;
&lt;li&gt;Where is the seller?&lt;/li&gt;
&lt;li&gt;What condition is the item in?&lt;/li&gt;
&lt;li&gt;Has the price changed?&lt;/li&gt;
&lt;li&gt;Is this listing unusually similar to another listing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those questions are visible in a basic product-card scrape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;p&gt;I stopped designing the output around the page.&lt;/p&gt;

&lt;p&gt;I started designing it around decisions.&lt;/p&gt;

&lt;p&gt;The row became more explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"listing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"displayStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Available"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isSold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sellerCountry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Very good condition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceHistory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requiresManualReview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Less pretty.&lt;/p&gt;

&lt;p&gt;Much more useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;Selectors are not the product.&lt;/p&gt;

&lt;p&gt;The state model is the product.&lt;/p&gt;

&lt;p&gt;If the scraper cannot preserve state, it will make the dataset feel more certain than it really is.&lt;/p&gt;

&lt;p&gt;That is dangerous because clean JSON is easy to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist I use now
&lt;/h2&gt;

&lt;p&gt;Before I trust a marketplace scraper, I check whether it handles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;live items;&lt;/li&gt;
&lt;li&gt;sold items;&lt;/li&gt;
&lt;li&gt;disappearing items;&lt;/li&gt;
&lt;li&gt;seller country;&lt;/li&gt;
&lt;li&gt;page country;&lt;/li&gt;
&lt;li&gt;item condition;&lt;/li&gt;
&lt;li&gt;price history;&lt;/li&gt;
&lt;li&gt;duplicate or similar listings;&lt;/li&gt;
&lt;li&gt;manual-review signals.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If it does not, I treat the dataset as a starting point, not a decision layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;The broken version was easier to explain.&lt;/p&gt;

&lt;p&gt;"It scrapes product data" is simple.&lt;/p&gt;

&lt;p&gt;"It models marketplace state" takes longer.&lt;/p&gt;

&lt;p&gt;But the second one is the version I would actually use.&lt;/p&gt;

&lt;p&gt;Have you ever shipped a scraper that was technically correct but operationally useless?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>scraping</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Your Scraper Returns 200 OK and Still Lies: 7 Marketplace Checks I Use</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Fri, 12 Jun 2026 01:25:30 +0000</pubDate>
      <link>https://dev.to/datakaz/your-scraper-returns-200-ok-and-still-lies-7-marketplace-checks-i-use-51j7</link>
      <guid>https://dev.to/datakaz/your-scraper-returns-200-ok-and-still-lies-7-marketplace-checks-i-use-51j7</guid>
      <description>&lt;p&gt;The easiest scraper bug to miss is not a crash.&lt;/p&gt;

&lt;p&gt;It is a clean result that is wrong.&lt;/p&gt;

&lt;p&gt;The request returns &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The selector works.&lt;/p&gt;

&lt;p&gt;The dataset has rows.&lt;/p&gt;

&lt;p&gt;The dashboard looks alive.&lt;/p&gt;

&lt;p&gt;And still, the output can lie.&lt;/p&gt;

&lt;p&gt;That happens often on marketplaces because a marketplace is not just a list of product cards. It is a changing state machine.&lt;/p&gt;

&lt;p&gt;Here is the checklist I now use before trusting marketplace scraping data.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate live records from sold records
&lt;/h2&gt;

&lt;p&gt;Active listings and sold listings answer different questions.&lt;/p&gt;

&lt;p&gt;Active listings show supply.&lt;/p&gt;

&lt;p&gt;Sold listings show demand.&lt;/p&gt;

&lt;p&gt;If both go into one dataset without a clear &lt;code&gt;recordType&lt;/code&gt;, later analysis gets unreliable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"listing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isSold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sold_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isSold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Treat disappearance as an event
&lt;/h2&gt;

&lt;p&gt;If a listing disappears, do not silently delete it from your mental model.&lt;/p&gt;

&lt;p&gt;Emit a tracking record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tracking_event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"previously_seen_listing_missing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listingId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may not know whether it sold, expired, or was removed.&lt;/p&gt;

&lt;p&gt;But you do know something changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Store page country and seller country separately
&lt;/h2&gt;

&lt;p&gt;This one sounds minor until it breaks your analysis.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;country&lt;/code&gt; can mean the market page you searched.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sellerCountry&lt;/code&gt; can mean the seller location exposed by the listing.&lt;/p&gt;

&lt;p&gt;Those are not the same thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sellerCountry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FR"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you merge them, your cross-market analysis becomes noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Add precision filters after broad search
&lt;/h2&gt;

&lt;p&gt;Marketplace search is optimized for discovery, not clean datasets.&lt;/p&gt;

&lt;p&gt;A broad query often returns nearby products.&lt;/p&gt;

&lt;p&gt;So I like a second layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchTerms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"classic flap"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requiredKeywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"classic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flap"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not make search perfect.&lt;/p&gt;

&lt;p&gt;It makes the dataset easier to inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Make condition a first-class field
&lt;/h2&gt;

&lt;p&gt;Price without condition is incomplete.&lt;/p&gt;

&lt;p&gt;For resale marketplaces, condition changes the meaning of every price.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Very good condition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conditionSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"marketplace"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If condition is hidden in a description string, you will forget to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Track price history, not only price
&lt;/h2&gt;

&lt;p&gt;One scrape tells you what existed.&lt;/p&gt;

&lt;p&gt;Repeated scrapes tell you what changed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceHistory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"observedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-01T12:00:00.000Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"observedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-08T12:00:00.000Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For monitoring, the delta is usually more useful than the snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Use risk signals as review queues
&lt;/h2&gt;

&lt;p&gt;Do not make accusations from scraper output.&lt;/p&gt;

&lt;p&gt;If listings look suspiciously similar, surface them as review candidates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"risk_signal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signalType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"similar_listing_cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"review_required"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives a human a useful queue without pretending the scraper has certainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real test
&lt;/h2&gt;

&lt;p&gt;I do not trust a marketplace scraper because it extracts rows.&lt;/p&gt;

&lt;p&gt;I trust it when it preserves enough context to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what exists now?&lt;/li&gt;
&lt;li&gt;what sold?&lt;/li&gt;
&lt;li&gt;what disappeared?&lt;/li&gt;
&lt;li&gt;where was it found?&lt;/li&gt;
&lt;li&gt;where is the seller?&lt;/li&gt;
&lt;li&gt;did the price change?&lt;/li&gt;
&lt;li&gt;what needs manual review?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the difference between scraping a page and modeling a marketplace.&lt;/p&gt;

&lt;p&gt;What check would you add before trusting marketplace data in production?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>scraping</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>AI Agents Make Scraping Look Easy. Marketplace State Is Where They Lie.</title>
      <dc:creator>KazKN</dc:creator>
      <pubDate>Fri, 12 Jun 2026 01:25:28 +0000</pubDate>
      <link>https://dev.to/datakaz/ai-agents-make-scraping-look-easy-marketplace-state-is-where-they-lie-56hk</link>
      <guid>https://dev.to/datakaz/ai-agents-make-scraping-look-easy-marketplace-state-is-where-they-lie-56hk</guid>
      <description>&lt;p&gt;AI agents make scraping look easy.&lt;/p&gt;

&lt;p&gt;Give the agent a page.&lt;/p&gt;

&lt;p&gt;Ask for JSON.&lt;/p&gt;

&lt;p&gt;Get back a clean object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Leather bag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/item/123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That feels useful.&lt;/p&gt;

&lt;p&gt;It is often not enough.&lt;/p&gt;

&lt;p&gt;The hard part of marketplace scraping is not always extracting the card.&lt;/p&gt;

&lt;p&gt;The hard part is knowing whether the card still means what you think it means.&lt;/p&gt;

&lt;h2&gt;
  
  
  200 OK can still be wrong
&lt;/h2&gt;

&lt;p&gt;A scraper can return a valid HTTP response, parse the page, extract the title and price, and still give you bad operational data.&lt;/p&gt;

&lt;p&gt;The page did load.&lt;/p&gt;

&lt;p&gt;The selector did work.&lt;/p&gt;

&lt;p&gt;The JSON did validate.&lt;/p&gt;

&lt;p&gt;But the marketplace state may be wrong.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The item is visible in search but already sold.&lt;/li&gt;
&lt;li&gt;The item disappeared, but you do not know whether it sold or was deleted.&lt;/li&gt;
&lt;li&gt;The same listing appears across several market locales.&lt;/li&gt;
&lt;li&gt;The seller country matters more than the page country.&lt;/li&gt;
&lt;li&gt;The search term matches a nearby model, not the model you wanted.&lt;/li&gt;
&lt;li&gt;The current price looks low, but the item condition explains why.&lt;/li&gt;
&lt;li&gt;The product card hides details that only exist on the item page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where AI extraction can create false confidence.&lt;/p&gt;

&lt;p&gt;It makes the data look cleaner than the marketplace really is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake: treating marketplaces like catalogs
&lt;/h2&gt;

&lt;p&gt;A catalog has products.&lt;/p&gt;

&lt;p&gt;A marketplace has state.&lt;/p&gt;

&lt;p&gt;For a catalog scrape, this might be enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Product name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"availability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"In stock"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a resale marketplace, I want a different shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"listing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listingId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"displayStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Available"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isSold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sellerCountry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Very good condition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceHistory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"observedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-01T12:00:00.000Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"observedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-08T12:00:00.000Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requiresManualReview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That object is less pretty.&lt;/p&gt;

&lt;p&gt;It is also more honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven checks I use now
&lt;/h2&gt;

&lt;p&gt;Before trusting marketplace data, I want seven checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Live and sold records should not be mixed
&lt;/h3&gt;

&lt;p&gt;Active listings tell you supply.&lt;/p&gt;

&lt;p&gt;Sold listings tell you demand.&lt;/p&gt;

&lt;p&gt;If both are pushed into the same flat dataset without a clear &lt;code&gt;recordType&lt;/code&gt;, analysis becomes messy quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Disappearance is a signal
&lt;/h3&gt;

&lt;p&gt;If an item was present yesterday and missing today, the scraper should not silently forget it.&lt;/p&gt;

&lt;p&gt;It should emit a tracking record.&lt;/p&gt;

&lt;p&gt;Something changed.&lt;/p&gt;

&lt;p&gt;That change may be more useful than another active listing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Page country is not seller country
&lt;/h3&gt;

&lt;p&gt;On international marketplaces, the locale you searched and the seller location are different concepts.&lt;/p&gt;

&lt;p&gt;If you only store one &lt;code&gt;country&lt;/code&gt; field, you will eventually confuse yourself.&lt;/p&gt;

&lt;p&gt;I prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sellerCountry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IT"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One tells me where I searched.&lt;/p&gt;

&lt;p&gt;The other tells me where the seller appears to be.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Search terms need precision filters
&lt;/h3&gt;

&lt;p&gt;Marketplace search is fuzzy.&lt;/p&gt;

&lt;p&gt;That is useful for browsing.&lt;/p&gt;

&lt;p&gt;It is dangerous for automation.&lt;/p&gt;

&lt;p&gt;If the query is &lt;code&gt;classic flap&lt;/code&gt;, I want the option to require those words in the result, not merely trust the search engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Condition changes the meaning of price
&lt;/h3&gt;

&lt;p&gt;A low price without condition is not a deal.&lt;/p&gt;

&lt;p&gt;It is just incomplete data.&lt;/p&gt;

&lt;p&gt;I want &lt;code&gt;condition&lt;/code&gt; and &lt;code&gt;conditionSource&lt;/code&gt; visible in the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Price history matters more than current price
&lt;/h3&gt;

&lt;p&gt;The current price is a snapshot.&lt;/p&gt;

&lt;p&gt;The price movement is the signal.&lt;/p&gt;

&lt;p&gt;If a scraper runs repeatedly, it should preserve enough state to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;did the price drop?&lt;/li&gt;
&lt;li&gt;did it rise?&lt;/li&gt;
&lt;li&gt;how long has it stayed at this level?&lt;/li&gt;
&lt;li&gt;did it disappear after the drop?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. Risk signals should be review queues, not accusations
&lt;/h3&gt;

&lt;p&gt;If two listings look unusually similar, that is worth surfacing.&lt;/p&gt;

&lt;p&gt;But a scraper should not declare fraud.&lt;/p&gt;

&lt;p&gt;The safer output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recordType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"risk_signal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"signalType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"similar_listing_cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"review_required"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives the human a queue.&lt;/p&gt;

&lt;p&gt;It does not pretend the scraper knows more than it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI helps
&lt;/h2&gt;

&lt;p&gt;AI can still help a lot.&lt;/p&gt;

&lt;p&gt;It can normalize messy titles.&lt;/p&gt;

&lt;p&gt;It can classify categories.&lt;/p&gt;

&lt;p&gt;It can summarize product descriptions.&lt;/p&gt;

&lt;p&gt;It can help detect weird edge cases in extracted records.&lt;/p&gt;

&lt;p&gt;But I do not want AI to hide uncertainty.&lt;/p&gt;

&lt;p&gt;For marketplace scraping, the best output is not the cleanest JSON.&lt;/p&gt;

&lt;p&gt;The best output is the JSON that preserves enough context to make a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  My rule now
&lt;/h2&gt;

&lt;p&gt;If a scraper cannot explain state, I do not trust its clean output.&lt;/p&gt;

&lt;p&gt;I want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what exists now;&lt;/li&gt;
&lt;li&gt;what disappeared;&lt;/li&gt;
&lt;li&gt;what sold;&lt;/li&gt;
&lt;li&gt;where it was found;&lt;/li&gt;
&lt;li&gt;where the seller appears to be;&lt;/li&gt;
&lt;li&gt;how the price changed;&lt;/li&gt;
&lt;li&gt;what needs manual review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Selectors are only the first layer.&lt;/p&gt;

&lt;p&gt;The real product is the state model.&lt;/p&gt;

&lt;p&gt;What is the most dangerous false-positive your scraper has returned?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>discuss</category>
      <category>scraping</category>
    </item>
  </channel>
</rss>
