Колдонуучу:KETC Parser for WikiMediaExport
AI for Encyclopedic Text Processing
https://gemini.google.com/app/e313c8fb2b247381
AI text processing tools overview
UP: which AI tool would you recommend to process a formatted text in docx or odt format
Response: If you are referring to custom, proprietary, or highly specific data structures (like internal log layouts or formatted configuration files), ChatGPT Plus (GPT-4o) or Claude 3.5 Sonnet are the best tools.
Because "4matic," "dokies," and "Oddity" are not standard or widely recognized open-source file extensions, you will need an AI with strong code-writing and pattern-recognition capabilities. Both models can write a custom Python script using libraries like re (Regular Expressions) to parse the unique text layout, extract the required variables, and convert the data into standard formats like JSON or CSV.
UP: I meant docx format and odd format
Response: For DOCX and ODT (OpenDocument Text) formats, Claude 3.5 Sonnet and ChatGPT Plus (GPT-4o) are excellent choices because you can upload the files directly into the chat interface. They will read, analyze, and extract the text automatically.
#knowledge:
Alternatively, if you are processing these documents at scale using Python, you can use the Gemini API or OpenAI API alongside libraries like python-docx and odfpy to extract the raw text before feeding it to the AI.
UP: is open AI API is a free to
Response: No, the OpenAI API is not free. It operates on a pay-as-you-go basis, where you are charged per 1,000 tokens (roughly 750 words) processed. While new accounts sometimes receive a small, temporary trial credit, you must link a payment method to continue using it.
UP: does basic chat GPT process Word Documents
Response: Yes, the free tier of ChatGPT does support file uploads, including Word documents (.docx).
As of 2026, OpenAI has expanded the capabilities of the free plan to include file and image uploads, allowing you to analyze, summarize, or extract information from documents directly within the chat interface.
Key Things to Know
- Usage Limits: While you can upload files on the free plan, it is subject to stricter usage and rate limits compared to paid tiers (like Plus or Pro). If you exceed your limit, you may be prompted to wait until your usage quota resets.
- Functionality: You can upload documents to have the AI summarize them, extract specific data, or answer questions based on the text.
- Storage: Files you upload are generally saved to your Library within ChatGPT, though free users have a smaller storage capacity (typically 500 MB) compared to paid subscribers.
If you find yourself hitting limits frequently, you might consider if your workflow requires the higher volume access provided by a paid subscription, but for occasional document processing, the free tier will work for your needs.
Is there a specific type of information you are trying to extract from these documents, or are you looking for a way to process a large volume of them at once?
Sources
- ChatGPT Free Tier FAQ - OpenAI Help Center
- ChatGPT free version: 6 features you can try right away in 2025 | Mashable
- File storage and Library in ChatGPT | OpenAI Help Center
Appendix: MisPrompts but useful
UP: OK Google
Response: It sounds like you might be thinking of "Hey Google" or the Google Assistant! While I am Gemini, Google's AI, I am here to help you just the same.
To clarify the current capabilities for the free version of ChatGPT (as of May 2026):
- File Uploads: Yes, the free tier of ChatGPT allows you to upload documents like .docx and .odt files.
- How it works: You can click the paperclip or "plus" icon in the chat bar to attach your document. Once uploaded, you can ask the AI to summarize, edit, or extract information from that specific file.
- Limitations: The free version does have usage caps. If you upload a very large number of files or perform very complex, high-volume analysis in a short period, you may eventually hit a limit and need to wait until your quota refreshes.
If you have a large number of documents to process and want a more automated approach, Google's Gemini (which you are using right now) also allows for document uploads and integration with Google Workspace if you are looking to pull information from your own Drive files.
Is there a specific task you were hoping to accomplish with those documents, or were you just checking to see what the free version could do?
UP: it's raised the second option I am I need to process large amount of files I need to pass the human language text to email format
Response: Processing a large volume of files requires moving away from manual chat-based uploads and toward automated pipelines. Since you are converting human-language text into a standardized email format, you need an architecture that extracts the text, structures it, and then generates the email output.
For this scale, you should look into Intelligent Document Processing (IDP) tools or build a custom automation pipeline.
1. The "Low-Code" Automation Route (Easier to Start)
If you aren't a developer, tools like Lido or Microsoft Power Automate are designed to handle exactly this type of "batch process to email" workflow.
- Lido: Excellent for mixed batches of documents. You can set up an email trigger (e.g., forward emails to a specific address), have the AI extract the necessary text, and then use its built-in automation to format and send or save that data as an email draft.
- Microsoft Power Automate (with AI Builder): If you are in a corporate environment, this integrates directly with Word and Outlook. You can create a flow where files placed in a specific folder are automatically parsed by an AI model, and the resulting structured data is dropped into an email template.
2. The "Developer" Route (Best for Customization & Scale)
If you have a very high volume (thousands of documents) or require a high level of customization, building a custom script using an AI API is the most cost-effective and powerful solution.
The Pipeline:
- Extraction: Use a library like python-docx or odfpy to pull raw text from your files.
- Processing: Send that text to the Gemini API (or OpenAI API). You would use a system prompt like: "Extract the key information from this document and format it into a professional email draft with a subject line and body text."
- Delivery: Use a simple SMTP library (or an API like SendGrid/Gmail API) to automatically save the output as a draft in your email client or send it directly.
Why this works:
- Cost: API calls are much cheaper than subscription fees when processing high volumes.
- Accuracy: You can give the model specific "Few-Shot" examples (showing it exactly how a document should be converted into an email) so it remains consistent across thousands of files.
- Control: You can build error handling (e.g., if a file is unreadable, it flags it for human review rather than breaking the whole batch).
Summary of Approaches
| Approach | Best For | Technical Skill |
|---|---|---|
| No-Code Tools (Lido, Power Automate) | Small-to-medium teams, rapid setup | Low |
| Custom Python API Script | High-volume, complex formatting, long-term costs | Medium/High |
Recommendation for your next step: If you have a modest budget and need something working today, look at Lido or similar "AI-powered email attachment parsers." If you have technical resources or are comfortable with Python, building a custom pipeline using the Gemini API will give you the most flexibility and the lowest long-term operational cost.
To help you decide, about how many documents are you needing to process per week or month?
UP: you misunderstood me not email but it's email
Response: My apologies for the confusion! Let me clarify.
If your goal is to standardize messy text from Word (.docx) or OpenDocument (.odt) files into a specific, professional email format (the actual content and layout of an email), the logic remains the same, but the focus shifts to formatting and tone consistency.
Since you are dealing with a large volume of files, here is the best way to handle this workflow:
1. The Strategy: "Template-Driven Transformation"
When processing large numbers of files, you should not just ask the AI to "write an email." Instead, use System Prompting to ensure every document is converted into the exact same structure.
The Workflow:
- Extract: Use a Python script to pull the text from the document.
- Transform: Pass that text to an AI model (like the Gemini API) with a strict instruction set.
- Example Prompt: "You are an assistant that converts internal reports into professional emails. Extract the project status, deadlines, and action items from the provided text. Return ONLY the output in this format: [Subject Line] followed by [Email Body]. Keep the tone professional and concise."
- Standardize: This ensures that no matter what the source file looks like, the email output is always uniform.
2. Recommended Tools for Volume
Since you have a large number of files, manual uploading to ChatGPT (even if it's free) will be too tedious because you have to do it one by one.
- Google Colab (Free to use): You can write a Python script in a Google Colab notebook. It allows you to loop through a folder of hundreds of .docx files, extract the text, send it to an AI model (like Gemini, which has a generous free tier for developers), and save the results into a single spreadsheet or a series of text files.
- Gemini API (Google AI Studio): If you are comfortable with a little bit of code, you can use Google AI Studio. It is free to use for a significant number of requests per minute, which is perfect for processing large batches of text without paying for an OpenAI subscription.
3. Why this works better than manual chat
- Consistency: You won't have to worry about the AI changing its "style" halfway through your files.
- Speed: You can process 100 documents in minutes rather than hours.
- No File Limits: You aren't constrained by the "chat" upload limits.
If you are not a programmer: You can use Make.com or Zapier. These tools allow you to create a "Watch Folder" in Google Drive or OneDrive.
- The flow: Whenever a new .docx file is saved in that folder → The tool sends the text to an AI (like ChatGPT or Gemini) → The tool takes the output and creates a draft email in your Outlook or Gmail account.
To give you the most accurate starting point: Are you comfortable using a little bit of Python code to automate this, or would you prefer a "drag-and-drop" software tool that does this for you?
docx/odt files to XML
UP: Not email but xml format
Response: Understood. Converting large volumes of .docx and .odt files into XML format requires a "Document Transformation" pipeline rather than a standard chat interface. XML is a structured format, so consistency is key.
Because you are processing a large volume of files, you need a workflow that handles the Structure (Extraction) and the Schema (XML Mapping) simultaneously.
The Recommended Pipeline: "The Automated Parser"
Since you are not just summarizing but transforming data into a machine-readable format (XML), I recommend using a Python-based approach. It is the most reliable way to ensure your XML tags are correctly closed and your hierarchy is consistent.
1. The Workflow Architecture
- Ingestion: Your script reads a directory containing your .docx and .odt files.
- Extraction: Using python-docx (for Word) and odfpy (for OpenDocument), the script pulls the raw text and metadata.
- Transformation (The AI Step): The text is sent to the Gemini API. You use a specific prompt: "Convert the following text into valid XML format using these tags: , , , . Ensure the output is strictly valid XML."
- Validation: The script saves the output as an .xml file.
2. Why this is the best path for XML
- Schema Enforcement: You can force the AI to return only the XML code block. You can even provide an XSD (XML Schema Definition) in the system prompt to ensure the output strictly follows your required structure.
- Error Handling: If the AI generates an error or an invalid XML tag, a script can automatically flag that file for your review, whereas manual copying-and-pasting would leave you with broken files.
- Scale: You can process 1,000 files in a single batch, which would be impossible to do manually in a chat interface.
3. Tooling Options
- Google AI Studio (Recommended): Use this to test your prompts. You can paste your document text and your desired XML schema to see how accurately the model formats it. Once it works, you can get an API key to use in your automation script.
- Python + BeautifulSoup / lxml: If you need to manipulate the XML after it is generated (e.g., adding specific attributes or tags), these libraries are the industry standard for handling XML in Python.
- No-Code Alternative: If you do not want to write code, look at Make.com. You can create a module that:
- Watches a Google Drive folder.
- Sends the file content to an AI model (OpenAI or Gemini module).
- Saves the AI's output as an .xml file back to your Drive.
Implementation Example (Conceptual)
If you were to set this up in a simple Python script, the interaction logic would look like this:
# Pseudo-code for your process
for file in folder:
text = extract_text(file)
xml_output = gemini_api.generate(
prompt="Convert this text to XML: " + text,
system_instruction="Output strictly valid XML."
)
save_to_file(xml_output, f"{file.name}.xml")
To determine if this is the right approach for your specific setup, how complex is the XML structure you need? (e.g., are you mapping simple data like "Name" and "Date," or does the XML need to follow a complex hierarchy with nested nodes and attributes?)
UP: it's well structured encyclopedic articles and I want to annotate each article in Ishmael format
Response: It appears there may be a misunderstanding regarding "Ishmael format." Based on technical standards, there is no widely recognized document annotation format known as "Ishmael." The term "Ishmael" is most prominently associated with specialized acoustic analysis software (used for marine mammal research) or individual software library names, rather than a document markup or text annotation standard.
If you are looking to annotate well-structured, encyclopedic articles into a machine-readable format, you are likely looking for one of the industry-standard markup languages used for linguistic and encyclopedic annotation.
Recommended Standards for Encyclopedic Annotation
If your goal is to make your articles interoperable and annotated, you should use one of the following:
- TEI (Text Encoding Initiative): This is the "gold standard" for academic and encyclopedic text annotation. It is a highly robust XML-based framework designed specifically for representing the structural, linguistic, and metadata characteristics of complex texts.
- JSON-LD (JSON for Linked Data): If you want your annotations to be "machine-understandable" for search engines or knowledge graphs, JSON-LD is the modern way to embed semantic meaning into your content.
- DocBook: Frequently used for technical, structured documentation and articles. It provides a clear schema for headers, sections, paragraphs, and lists.
Suggested Workflow for Large-Scale Annotation
Since you need to process a large volume of documents and require XML-based output, you should use an AI-Assisted Parsing Pipeline.
- Define your Schema: Create a small sample of what you want your XML/annotated output to look like. (e.g., <article><header>...</header><body_text>...</body_text><annotation type="concept">...</annotation></article>).
- Use an AI Pipeline for Mapping:
- Use a Python script with an LLM API (like Gemini).
- Prompt: Provide the AI with your raw .docx or .odt text and your target XML schema.
- Instruction: "Extract the encyclopedic content and wrap it in the following XML tags: [Your Schema]. Identify key entities and annotate them with [concept] tags."
- Validate: Always run the output through an XML Validator (using Python's lxml library) to ensure the tags are correctly closed and the hierarchy is valid before saving.
Could you please clarify where you heard of the "Ishmael" format, or provide an example of what an annotated snippet looks like? If you can provide a small sample of the "Ishmael" style you are aiming for, I can help you write a Python script or define a prompt that replicates that specific structural pattern for your entire library of articles.
Sources
- Ishmael : 1.0 user's guide ; Ishmael : integrated system for holistic multi-channel acoustic exploration and localization - the NOAA Institutional Repository
StarDict Schema
UP: I'm using StarDict xml schema
Response: That clarifies things significantly. You are working with StarDict's textual (XML-based) dictionary format.
Since you are dealing with "well-structured, encyclopedic articles" that you need to annotate and convert into this XML schema at scale, a manual approach in a chat interface will not be efficient. You need a scripted transformation pipeline.
The Recommended Workflow
Because StarDict is a strict format (usually involving an .ifo info file, an .idx index, and a .dict body file), you need to transform your Word/ODT files into the specific XML structure that tools like stardict-tools or makedict can then compile into the final binary dictionary.
1. Pre-processing (Extraction)
Don't try to have the AI process the document in one go.
- Step A: Use a Python script with python-docx (for .docx) or odfpy (for .odt) to extract the raw text content of each encyclopedic entry.
- Step B: Ensure each file or section is clearly associated with a "Headword" (the dictionary term).
2. The AI Transformation (The Annotation Engine)
Use the Gemini API as an "annotator" to map your content into the StarDict XML schema.
- The System Prompt: Feed the model the exact XML structure of the StarDict article format."You are an expert at encoding encyclopedic articles into StarDict XML format. Given the provided raw text, identify the headword and format the definition according to the following XML schema: <article><k>HEADWORD</k><def>DEFINITION_CONTENT</def></article>. Ensure all special characters are escaped for XML."
3. Automation Script (The Pipeline)
You can create a Python loop to process your entire folder of files:
import os
from docx import Document
import google.generativeai as genai
# Setup Gemini API
genai.configure(api_key="YOUR_API_KEY")
model = genai.GenerativeModel('gemini-1.5-flash')
# Loop through all files in your folder
for filename in os.listdir('your_directory'):
if filename.endswith(".docx"):
# 1. Extract raw text
doc = Document(filename)
raw_text = "\n".join([para.text for para in doc.paragraphs])
# 2. Transform via AI
prompt = f"Convert this text to StarDict XML format:\n{raw_text}"
response = model.generate_content(prompt)
# 3. Save as XML
with open(f"{filename}.xml", "w", encoding="utf-8") as f:
f.write(response.text)
Why this is the best approach for you:
- Strict Schema Adherence: By providing the StarDict schema in your system prompt, you ensure the AI doesn't just "summarize," but actually "tags" the content.
- Scalability: You can run this script on 1,000 files in a few minutes using the API.
- Error Handling: You can easily add code to verify that the generated XML is well-formed (e.g., using lxml.etree in Python to validate it against a DTD or XSD if you have one).
- Final Compilation: Once you have your batch of XML files, use the makedict utility or stardict-tools (available on most Linux repositories or via source) to compile your .xml files into the final StarDict binary format (.ifo, .idx, .dict) that your dictionary reader software actually uses.
Crucial Tip for Encyclopedic Content:
Since your articles are well-structured, look for the XDXF (XML Dictionary Exchange Format). Many tools that convert to StarDict prefer XDXF as an intermediate step because it is more descriptive and handles encyclopedic metadata (like cross-references, authors, and formatting) better than the basic StarDict textual format.
Are you comfortable using Python to run this loop, or would you like me to help you refine the specific XML structure for your dictionary articles?
MediaWiki Export Schema. The Problem
UP: Actually the final xml structure is Mediawiki Export xml file...
which I get from simple conversion from StarDict xml. So the initial text can be directly annotated to that xml schema. The main tag is mediawiki. Then come a series of pages entries. In my case there is only one initial revision, as far as i remember in text tag. Bold and italic fonts remain in html. Section titles embraced in double equations.
Response: Since you are targeting a MediaWiki Export XML schema (the format used by Wikipedia and internal Wiki systems), your task is effectively semantic transformation. You are mapping your source text—complete with embedded HTML formatting and specific section syntax—directly into the MediaWiki import format.
The Target Schema Structure
To process your documents, your pipeline needs to produce an XML output that mimics the structure required by the MediaWiki importDump.php tool:
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/">
<page>
<title>Article Title</title>
<revision>
<text xml:space="preserve">
</text>
</revision>
</page>
</mediawiki>
The Strategy: "Schema-Driven Transformation"
Since your articles are well-structured, you should use an AI pipeline that treats your source document as an "Entity" to be encapsulated into the MediaWiki <page> tag.
1. The Prompting Strategy
Because the AI needs to preserve specific formatting (like ==Sections== and <b>/<i> tags), you must use a Few-Shot Prompt. This tells the AI precisely how to handle the transformation without stripping your required formatting.
System Prompt Example: "You are a data conversion engine. Your task is to convert encyclopedic text into MediaWiki XML format. Rules:
- Wrap the entire output in <mediawiki>.
- Each article goes inside a <page> tag.
- Use the first line as the <title>.
- Keep all bold/italic text in HTML (, ).
- Keep section titles wrapped in double equal signs (Title).
- Do not wrap the text in Markdown code blocks; output raw XML."
2. Why this is the best approach for this Schema
- HTML Preservation: Unlike general text parsers, LLMs (via the API) are excellent at preserving specific markup patterns like <b> or ==Title== while ignoring irrelevant surrounding whitespace.
- Batching: You can pass a list of articles to the API in one request (if they are short enough) or iterate through your library, ensuring the <mediawiki> parent tag is managed by your script.
- Schema Integrity: By explicitly stating the schema in the prompt, you prevent the AI from "summarizing" the text; it will treat the content as a direct translation of format.
3. Recommended Implementation
If you are processing a large directory of files, use the Google AI Studio (Gemini API) to iterate through the files:
# Conceptual loop for your process
def create_mediawiki_xml(text, title):
# This structure is easily handled by Gemini's long context window
return f"""
<page>
<title>{title}</title>
<revision>
<text xml:space="preserve">{text}</text>
</revision>
</page>
"""
# Use your script to aggregate these pages into the main <mediawiki> tag.
Pro-Tips for MediaWiki Compatibility
- XML Escaping: Ensure your script handles character escaping (e.g., converting & to & or < to <) in the text, otherwise your generated XML will fail the MediaWiki import.
- Validation: Before running a bulk import into a real MediaWiki installation, use a simple xmllint command in your terminal to ensure your generated XML is well-formed: xmllint --noout your_output_file.xml
- Handling Section Titles: Since you mentioned your titles are in double equals, ensure the AI doesn't convert them to XML tags (like <section>). Keep them as plain text with == to maintain compatibility with the MediaWiki parser.
Does your current collection of documents contain any embedded images or complex tables, or is it strictly text-based encyclopedic entries? (This will determine if you need to add logic to handle [[File:Image.jpg]] or MediaWiki-style table syntax).
UP: I also want to preserve text formatting inside the text tag
Response: To preserve your text formatting (bolding, italics, section headers) within the <text> tag of a MediaWiki Export XML file, you must treat the content inside that tag as Wikitext (or a mix of Wikitext and allowed HTML).
Because you are generating this XML programmatically, you must ensure your content is "escaped" correctly so that the XML itself remains valid.
1. The Strategy for "Safe" Formatting
When you put content inside <text xml:space="preserve">...</text>, you are telling MediaWiki to treat that block as raw source code. To ensure formatting survives the transfer:
- Bold & Italics: MediaWiki prefers triple single-quotes ('''bold''') and double single-quotes (''italic'') over HTML <b> and <i> tags. While MediaWiki does support a subset of HTML, using the native Wikitext markup is more robust for imports.
- Sections: Use double equals (==Section Title==) exactly as you planned. These are native Wikitext and do not need to be wrapped in HTML.
- XML Escaping (CRITICAL): This is where most automated conversions fail. You cannot simply put raw &, <, or > characters inside your XML tags. Your script must escape them:
- & becomes &
- < becomes <
- > becomes >
2. Handling the Transformation Logic
If you are using Python, you should use the xml.sax.saxutils.escape function to handle the characters, but keep your specific formatting symbols (' and =) intact.
Here is how your logic should structure the content:
The "Template" for your Generator Script:
import xml.sax.saxutils as saxutils
def format_mediawiki_page(title, content):
# 'content' here should already have your ==Headers==, '''bold''', and ''italic''
# We escape standard XML characters but leave our formatting markup alone
safe_content = saxutils.escape(content)
return f""" <page>
<title>{title}</title>
<revision>
<text xml:space="preserve">{safe_content}</text>
</revision>
</page>"""
3. Why this works
- xml:space="preserve": This attribute ensures that the MediaWiki importer respects your line breaks and indentation. Without it, the importer might strip leading spaces or collapse your paragraphs.
- The Sanitizer: MediaWiki's import engine has a "Sanitizer.php" component. By using native Wikitext (single quotes for bold/italic, double equals for sections) and properly escaped XML, you ensure the content passes through the sanitizer without being stripped or altered.
- HTML Subset: If you must use HTML (e.g., <b> or <i> that you already have in your source files), MediaWiki will generally accept them as long as they are part of the "allowed" subset (which includes <b>, <i>, <code>, <blockquote>, etc.). Just ensure the brackets around those tags are also escaped as < and > in the XML file itself.
Important Warning for Large Imports
If you have a massive amount of articles, do not try to build one single giant XML file.
- Memory limits: The XML parser will run out of RAM if the file is too large.
- Chunking: It is best practice to generate XML files containing 50–100 articles each and run the importDump.php script on each one.
Are you planning to write the Python script to perform this conversion, or would you like me to help you draft the full script structure for a folder of documents?
UP: There will be super and subscripts in the text as well
Response: When dealing with superscripts and subscripts in a MediaWiki XML export, you have two primary options: HTML tags (most common for compatibility) or Wikitext/LaTeX markup. Since you are aiming for a robust encyclopedic format, here is the best way to handle them.
1. The Markup Approach
MediaWiki natively supports sub- and superscripts using HTML tags. Because you are already generating the XML, using these tags is the most reliable way to ensure the formatting survives the import process.
- Subscript: <sub>text</sub>
- Superscript: <sup>text</sup>
2. Handling the XML Pipeline
Since you already have an automated pipeline, you need to add a "Sanitization & Tag Mapping" layer. Before the text hits your XML generator, your script should ensure that any superscripts or subscripts in your source (.docx/.odt) are converted to these HTML tags.
Revised Processing Workflow:
- Extract: Your script reads the Word/ODT file.
- Tag Detection: Use a library like python-docx to inspect the run styles (the specific formatting of a text segment). If a run is marked as sub or super, wrap the text in <sub> or <sup> tags.
- Escape: Run your XML character escape (handling &, <, >). Crucial: Ensure your script applies the escape function after adding the <sub>/<sup> tags.
- Inject: Place the result into the <text> element of your XML page.
3. Implementation Logic (Python snippet)
When you iterate through the paragraphs in your documents, you can identify these styles easily:
from docx import Document
import xml.sax.saxutils as saxutils
def get_text_with_formatting(paragraph):
result = ""
for run in paragraph.runs:
text = run.text
# Check for subscript or superscript in docx styling
if run.font.subscript:
text = f"<sub>{text}</sub>"
elif run.font.superscript:
text = f"<sup>{text}</s>"
# Now escape the XML characters in this segment
result += saxutils.escape(text)
return result
4. Important Considerations for MediaWiki
- Math Mode: If your encyclopedic articles are highly scientific (e.g., E=mc2), MediaWiki has a built-in <math> extension that uses LaTeX. If your documents contain complex chemical or mathematical formulas, it is often better to convert those specifically to <math>...</math> tags rather than using simple <sub>/<sup> tags, as it provides better rendering and professional-grade typography.
- Sanitization: MediaWiki’s internal parser is strict. If you have nested tags (e.g., a superscript inside a bold tag: <b><sup>1</sup></b>), ensure your generator script produces valid, closed tags in the correct order.
- Special Characters: If your source text contains characters like ©, ™, or ±, ensure your Python script is set to utf-8 encoding during the file read and the XML write, or the MediaWiki import will error out on those symbols.
By wrapping your sub/superscripts in these HTML tags before they enter the <text> element, they will render perfectly when you view the articles in your Wiki interface.
Do you have a preference for using standard <sub>/<sup> tags, or do you need to convert your math-heavy sections into LaTeX format for the <math> extension?
UP: Is CDATA allowed in mediawiki schema
Response: Yes, CDATA sections are allowed and are often recommended for use within the MediaWiki export XML format.
Why use CDATA?
When you are generating your XML programmatically, you are dealing with text that contains symbols like &, <, >, and other formatting characters (like the == for sections or ''' for bolding).
Normally, you would have to manually "escape" every single one of these characters (e.g., changing < to <) to avoid breaking the XML structure. By using a CDATA section, you tell the XML parser to treat everything inside as raw character data, allowing you to include these symbols without escaping them.
How it looks in your structure:
Instead of just placing your text inside the <text> tag, you wrap it in the CDATA declaration.
<page>
<title>Article Title</title>
<revision>
<text xml:space="preserve"><![CDATA[
== Section Title ==
This is a paragraph with <b>bold</b>, <i>italic</i>, and <sup>superscript</sup>.
You can use & or < > symbols here safely without escaping them.
]]></text>
</revision>
</page>
Important rules to remember:
- The Closing Sequence: You cannot include the string ]]> inside your actual content, as the parser will interpret it as the end of the CDATA section, which will break your XML. If your encyclopedic content happens to include that specific character sequence, you must either escape those characters or use a different method for that specific segment.
- xml:space="preserve": You should keep this attribute on your <text> tag even when using CDATA. It ensures that the MediaWiki importer respects your line breaks and indentation, which is crucial for your section titles and paragraph structure.
- Compatibility: MediaWiki’s importDump.php handles CDATA correctly. It essentially strips the <![CDATA[ and ]]> tags and feeds the internal content directly to the Wiki parser as Wikitext.
Recommendation for your Pipeline
Since you are already using Python to generate these files, using CDATA is actually easier than standard XML escaping. You won't need the xml.sax.saxutils.escape function anymore.
Your logic simplifies to:
def format_mediawiki_page_cdata(title, content):
return f""" <page>
<title>{title}</title>
<revision>
<text xml:space="preserve"><![CDATA[{content}]]></text>
</revision>
</page>"""
This method is cleaner and less prone to errors than manually replacing every special character in your encyclopedic articles.
Do you have any other specific concerns about the MediaWiki import format before you begin writing your automation script?
AI approach
UP: So, build me a holistic recipe to deal with such encyclopedic formatted text files each containing a series of a series of articles in Natural language
Response: To handle a large collection of files containing multiple encyclopedic articles each, you need a "Split-Extract-Format-Aggregate" pipeline. Since you are using CDATA, the XML generation part is straightforward, but the real challenge is accurately segmenting multiple articles from a single file.
Here is the holistic recipe to convert your library of documents into a single, valid mediawiki-export XML file.
The Holistic Pipeline Strategy
- Normalization (Pre-processing): Convert your .docx / .odt files into raw text strings while maintaining markers for bold, italic, sub/super, and headers.
- Segmentation (The Logic Splitter): Since your files contain a series of articles, you need a delimiter (e.g., a specific heading style or a separator like ---) to split the text into individual entry objects.
- Refinement (The AI Annotator): Send each extracted segment to the LLM to format it into compliant Wikitext (e.g., converting headers to ==Section==, and applying <sup>/<sub> tags).
- XML Assembly: Wrap the refined content into the <page> structure and consolidate everything within the root <mediawiki> tag.
The Recipe (Python Implementation)
You will need the python-docx library and an API key for a model like Gemini.
import os
from docx import Document
# 1. Extraction Function
def extract_text_with_styles(doc_path):
doc = Document(doc_path)
# Strategy: Store text with custom tags, e.g., <b_start>Bold<b_end>
# The AI will interpret these and turn them into Wikitext
full_text = ""
for para in doc.paragraphs:
# Implementation logic to capture run.bold, run.italic, run.font.subscript, etc.
# ... logic goes here ...
full_text += para.text + "\n"
return full_text
# 2. The AI Transformation Prompt
def refine_to_wikitext(raw_article_text):
prompt = f"""
Convert the following text into MediaWiki Wikitext format.
- Headers: Use ==Title==
- Bold: Use '''bold'''
- Italic: Use ''italic''
- Subscript/Superscript: Use <sub>/<sup> tags.
- Do not output markdown, just the clean wikitext.
Text: {raw_article_text}
"""
# Call Gemini API here
return response.text
# 3. Assemble into MediaWiki XML
def generate_xml_output(articles_list):
xml_header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/">'
xml_footer = '</mediawiki>'
pages = []
for title, content in articles_list:
pages.append(f"""
<page>
<title>{title}</title>
<revision>
<text xml:space="preserve"><![CDATA[{content}]]></text>
</revision>
</page>""")
return xml_header + "".join(pages) + xml_footer
Key Execution Tips
- Handling the "Series": If your files contain multiple articles, the biggest risk is the AI hallucinating or merging two entries. In your Segmentation step, ensure you split the file based on the document's inherent structure (e.g., look for Heading 1 styles) before passing the text to the AI.
- The "One-Pass" vs "Iterative" Approach: * If your articles are short, pass 5–10 articles to the AI in one API call to save costs.
- If your articles are long, process one article per API call to ensure the AI doesn't hit context limits or lose track of the specific XML schema.
- Validation: After generating the XML, do not try to open it in a standard text editor. Use xmllint to verify the structure. xmllint --noout output_file.xml If xmllint returns no errors, your CDATA blocks and tags are perfectly formed and ready for MediaWiki.
Final Checklist for your Wiki Import
- Character Encoding: Ensure your script opens and writes files with encoding='utf-8'.
- Article Metadata: Ensure you have a predictable way to extract the "Title" of the article; if the title is missing, the import will fail to create a distinct page.
- Batching: As mentioned earlier, split your final output into smaller XML files (e.g., 5MB–10MB each) for smoother ingestion via the web-based "Import" tool or the importDump.php command-line script.
Does this structure cover the complexity of your documents, or do you have specific "edge cases" (like cross-references between articles) that we need to account for?
UP: Can a splitter be done by AI
Response: Yes, an AI is exceptionally good at splitting text. In fact, AI-based splitting is currently considered the "gold standard" for complex tasks because, unlike traditional methods that just count characters or look for simple symbols (like periods or commas), an AI actually understands the content.
To help you understand how this works, here is a comparison between simple mechanical splitting and intelligent AI-based splitting:
Why use AI to split your articles?
If you have files containing a "series of articles," a standard "text splitter" would likely break an article in the middle of a sentence or separate a title from its body text. An AI, however, functions much more like a human editor:
- Semantic Awareness: The AI detects the "logical flow" of your text. It recognizes where one encyclopedic article ends and the next one begins based on context, tone, and structural cues (like shifts in topic or changes in subject matter).
- Context Preservation: When the AI splits your text, it ensures that each resulting segment is a "self-contained" unit. This is vital for your MediaWiki goal, as each page needs a distinct title and body.
- Structure Recognition: AI can identify specific structural elements—such as headers, lists, or tables—and use those as natural "break points" to ensure the integrity of the formatting.
How to implement an AI "Splitter"
You don't need a separate piece of software. You can incorporate the "splitting" logic directly into the same AI pipeline we discussed for your XML generation.
The Workflow:
- Feed the raw text: Pass a whole file (or a large chunk of it) to the AI.
- Define the task: Use a system prompt that tells the AI to act as a segmentation engine.
- Example Prompt: "You are a professional editor. Analyze the provided document, which contains multiple encyclopedic articles. Segment this document into individual articles. For each article, output a clear title followed by the full content, separated by a unique delimiter like ---ARTICLE_END---."
- Process and Convert: Once the AI has returned the text with these clear separators, your Python script can easily split the text into a list of "Article Objects" and proceed to the XML generation step we defined earlier.
When to use AI vs. Basic Logic
- Use Basic Logic (Python string splitting): If your articles are separated by a consistent, unambiguous delimiter (like a specific page break code, a unique character string, or a predictable header tag that always appears at the start of an article). This is faster and cheaper.
- Use AI Splitting: If your documents are messy, have inconsistent formatting, or rely on the "context" of the text to know where one article ends and the next begins.
Since you mentioned you have "well-structured, encyclopedic articles," I suspect you might be able to use a Hybrid Approach: Use a simple Python script to find the obvious section headers (the double equals ==Title==), and then use the AI to perform the final cleanup and XML annotation for each individual block.
Would you like me to draft a Python script that combines a basic "Title-based splitter" with an AI-based "XML annotator" to handle your documents?
Sources
- LLM-Based Chunking: Intelligent Text Splitting for Better RAG | Blog - Avesta Labs
- The Art of Text Splitting: From Basic to Advanced Techniques for Language Models
- What is Document Splitting AI? - LlamaIndex
UP: Draft me a Python script that combines a basic "Title-based splitter" with an AI-based "XML annotator" to handle my documents
Response: That sounds like a great plan. By using a hybrid approach—where you use a script to find the structural boundaries and the AI to handle the "semantic" heavy lifting of formatting and XML-tagging—you will get the highest quality output with the lowest risk of error.
Since you are ready, here is the structure for the Python script. This uses a "Split-then-Refine" logic.
The Hybrid Pipeline Script
import os
import re
import google.generativeai as genai
# Setup Gemini
genai.configure(api_key="YOUR_API_KEY")
model = genai.GenerativeModel('gemini-1.5-flash')
def split_articles(text):
# Using regex to find your section titles (assuming double equals ==Title==)
# This splits the document into a list of articles
articles = re.split(r'(==.*?==)', text)
# Logic to pair the title with the following body text goes here
return articles
def refine_and_annotate(title, content):
# This is where the AI converts the specific chunk to MediaWiki XML
prompt = f"""
Convert this article to MediaWiki XML.
Title: {title}
Content: {content}
Output only the <page>...</page> block using CDATA.
"""
response = model.generate_content(prompt)
return response.text
# 1. Main Loop
# Read your file, split it, iterate and annotate, then save to a final file.
Why this structure is efficient:
- Regex Splitter: By using a simple re.split for your ==Title== headers, you avoid sending unnecessary data to the AI. You are only sending the specific article content, which keeps your API costs low and ensures the AI doesn't get confused by "orphan" text.
- Modular Refinement: The refine_and_annotate function allows you to test your conversion on a single article before running it on the entire library. This is a critical debugging step.
- Clean Integration: Since the output is already <page> blocks, you can simply save them to a list and join them with the <mediawiki> header and footer at the very end of your script.
Next Steps for you:
- Test one file: Create a test .docx file with 3 articles. Run the script and see if the output format matches your desired MediaWiki import expectations.
- Check for "Edge Case" Characters: If your articles contain rare symbols, check how they appear in the final XML CDATA block.
- Run xmllint: As mentioned before, run that final check on your generated XML file to ensure the hierarchy is perfect.
Are you ready to start building the script, or would you like to refine the RegEx logic to match the exact way your titles are written in your documents?
UP: How to make AI to markup articles and their contents according to mediawiki export scheme. Let's start from one file with 100 articles
Response: To process 100 encyclopedic articles from a single document into a MediaWiki-compliant XML export, you need a structured "pipeline" that converts document formatting into Wikitext while maintaining the XML structure.
The Pipeline Architecture
- Ingestion: Extract raw text and structural metadata from your .docx or .odt files.
- Segmentation: Split the file into 100 distinct article units.
- Refinement (AI Engine): Convert raw content into Wikitext (headers, bold, italics) and wrap it in XML-compliant structures.
- Assembly: Aggregate all pages into the <mediawiki> root element.
Step-by-Step Implementation
1. Pre-processing and Segmentation
Do not pass the entire 100-article file to the AI at once. It may exceed context limits or cause "hallucinations" between articles. Instead, use a script to split the document. If your articles are structured with a consistent pattern (e.g., a specific heading style or a delimiter like ---), use Python to split them:
import re
# Assuming articles are separated by a double newline and a header
# Adjust the regex based on your file's structure
raw_text = get_docx_text("articles.docx")
article_list = re.split(r'\n\n(?===.*?==)', raw_text)
2. Refining with AI (Annotation Engine)
For each segment, send the content to the Gemini API with a system prompt that enforces Wikitext output. Wikitext is natively supported by MediaWiki and is much cleaner than injecting HTML tags.
- Bold: '''text'''
- Italic: ''text''
- Headers: ==Section Title==
- Sub/Superscripts: <sub>text</sub> / <sup>text</sup>
System Prompt:
"You are an automated editor. Convert the provided text into MediaWiki Wikitext. Maintain all section headers (==), bolding ('''), italics (''), and sub/superscript HTML tags. Output only the content of the article."
3. Generating the XML (with CDATA)
To avoid manual character escaping (which breaks easily with &, <, or >), wrap your article content in a <![CDATA[ ... ]]> block inside the <text> element.
Example of the generated structure for each article:
<page>
<title>Article Title</title>
<revision>
<text xml:space="preserve"><![CDATA[
== Introduction ==
This is a <b>bold</b> article with <sub>subscript</sub> elements.
]]></text>
</revision>
</page>
Key Recommendations for Success
- Validation: Use xmllint to verify your XML file is well-formed before trying to import it. xmllint --noout your_output_file.xml
- Batching: MediaWiki's importDump.php can struggle with massive files (like 100,000+ words). If the import fails or times out, split your 100 articles into 5 smaller XML files (20 articles each).
- CDATA Constraint: Ensure your content does not contain the sequence ]]>. If it does, the XML parser will terminate the CDATA block prematurely. You can add a simple Python check to replace ]]> with ] ] > if necessary.
Would you like me to draft the complete Python script that iterates through your file, interacts with the Gemini API to format each article, and writes the final XML?
UP: Actually segmentation is a major task. If I provide the input text and the annotated xml output. Can AI learn by itself how to segment and refine
Response: The short answer is yes, it can "learn" to do this through a technique called Few-Shot Prompting. You do not need to "train" or "fine-tune" the AI (which is expensive and complex). Instead, you can use In-Context Learning. By providing the AI with a handful of examples where it sees a "Raw Input" paired with the "Perfect XML Output," the model will infer the logic behind your segmentation, tagging, and formatting preferences.
How to "Teach" the AI via Few-Shot Prompting
When you process your 100 articles, you don't send just the raw text. You send a "meta-prompt" that includes your patterns.
1. Construct a "Pattern Library"
Create a small set of 3–5 representative examples. These examples should include the trickiest parts of your data (e.g., a case with subscripts, a case with cross-references, and a case with a very long title).
The prompt structure should look like this:
System: You are an expert at converting encyclopedic content to MediaWiki XML.
Follow the structure and formatting conventions shown in the examples below.
Example 1:
Input: [Raw text snippet 1]
Output: <page><title>...</title>...</page>
Example 2:
Input: [Raw text snippet 2]
Output: <page><title>...</title>...</page>
... (Include 3-5 such examples)
Now, process the following input article:
Input: [Your actual article content]
Output:
Why this works
- Pattern Recognition: Large Language Models (LLMs) are statistical engines. By seeing the output format alongside the input, the model identifies the transformation logic—how you handle ==Section Headers==, how you treat <sub> and <sup>, and where you choose to cut the text.
- Consistency: The model will stick to the format demonstrated in your examples. If you show it how to handle a specific type of section in your examples, it will replicate that behavior for the other 99 articles.
Practical Implementation Strategy for 100 Articles
Since you have 100 articles, do not try to put all 100 into one prompt. Instead, use a Python script to iterate:
- Preparation: Put your 3–5 "Golden Examples" (input/output pairs) into a text file or a dictionary in your code.
- The Loop: * Your script loads the next article.
- It constructs a prompt by grabbing the "Golden Examples" + the "Target Article."
- It sends this to the API.
- It saves the result.
- Refinement: If the AI makes a mistake on article #42, don't change the code. Update your "Golden Examples" to include that tricky article format. The AI will "learn" from the updated examples immediately.
Should you use Fine-Tuning?
In the vast majority of cases, No. Fine-tuning is for when you need the model to learn a complex style that you cannot fit into 5 examples, or to drastically reduce costs by using a smaller, specialized model. For structured data like MediaWiki XML, Few-Shot Prompting is almost always superior, faster, and easier to debug.
Important Note on Segmentation
If you are worried the AI won't know where to segment the articles, you can include this instruction in your system prompt:
"I will provide you with a block of text. Identify the start and end of each distinct encyclopedic entry. If the input contains multiple entries, process them individually and return each as a separate <page> block."
Would you like me to write a small Python template for you that incorporates this "Golden Examples" list, so you can test it on a few of your articles?
Sources
- Few-Shot Prompting: Techniques, Examples, and Best Practices - DigitalOcean
- Few Shot Prompting | Use Cases & Examples - QuillBot
- What is Few Shot Prompting? Complete Guide - Articsledge
- Few-Shot Prompting: Teaching AI With Just a Few Examples | by Akanksha Sinha - Medium
UP: I cannot understand 2. The Loop. I'm not going to separate target articles by myself. Do u mean I should feed AI chunks of the text?
Response: Yes, that is exactly what I mean. You do not need to manually segment the articles. You should let the AI do the segmentation for you.
When you have a long document, you can feed the AI "chunks" of that document, and tell it: "Here is a large block of text. Please identify where each article begins and ends, and for each one, produce the XML output."
Here is how to handle that without doing the work yourself:
The "Segment-and-Process" Workflow
Instead of a Python script that forces the split, your script will simply "stream" the document to the AI in large, manageable pieces (e.g., 5,000–10,000 characters at a time) and ask the AI to act as the parser.
1. The Strategy: "Sliding Window" Prompting
You don't need to cut the text perfectly. You simply send a large chunk of the document to the AI with a very specific instruction:
System Prompt: "I am providing you with a raw text document that contains a sequence of encyclopedic articles.
- Identify every individual article in this text.
- For each article found, extract the content and format it into the MediaWiki XML <page> structure I have provided in my examples.
- If an article is cut off at the end of this chunk, do not worry—just process what you can, and I will handle the overlap in the next pass."
2. Why the AI can do this better than you
- Pattern Recognition: Even if your articles aren't delimited by a perfect "---" symbol, the AI recognizes the structure of an encyclopedia. It sees the "Title" followed by "Definitions" and knows when the tone and subject shift to a new entry.
- Contextual Intelligence: If your articles are "well-structured," the AI is essentially scanning for the next logical "Heading" to start a new page. It is effectively performing the role of a human editor.
3. How to write the Python loop for this
You don't need to know where the articles are. You just need to tell your script to read the file in "chunks":
# A simple way to read your file in chunks of 5000 characters
def read_in_chunks(file_path, chunk_size=5000):
with open(file_path, 'r', encoding='utf-8') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
yield chunk
# Your main logic
for chunk in read_in_chunks("my_100_articles.txt"):
# Pass this chunk to the AI
# The AI will identify the articles inside this 5000-char block
# and return the XML for all articles it found.
xml_output = refine_and_annotate(chunk)
print(xml_output)
Addressing the "Cut-off" Problem
If you are worried the AI will cut an article in half because of the chunking, you can add a "Context Overlap" to your script.
- Tell your script to read 5,000 characters.
- Then, have it back up 500 characters and start the next chunk from there.
- This ensures that if an article was cut mid-sentence, the next chunk has the end of that sentence, allowing the AI to finish the article correctly.
Is this the right level of automation for you?
By using this method:
- You: Just point the script to the file.
- The Python Script: Reads the file in chunks.
- The AI: Identifies the articles within those chunks and generates the XML for them.
Does this feel like a more manageable approach for your 100-article file, or would you like to see how we can handle the "cut-off" issue more robustly?
UP: Can u explain me how I can use AI API with python. Will I need some subscription? What are the constraints
Response: To get started with an AI API in Python, you generally follow a standard pattern. Below is a guide on how it works, the costs, and the constraints.
1. The Workflow: How it works in Python
You don't need to "train" anything. You use a library (provided by the AI company) to send your text to their server, which then sends the structured response back.
Basic Setup:
- Get an API Key: Sign up at the provider's developer platform (e.g., Google AI Studio for Gemini or OpenAI Platform).
- Install the Library: Use pip to install the package (e.g., pip install google-generativeai).
- The Script:
import google.generativeai as genai
# Configure your API key
genai.configure(api_key="YOUR_ACTUAL_API_KEY")
# Choose your model (Flash is fast and cheap for bulk processing)
model = genai.GenerativeModel('gemini-1.5-flash')
# Send your prompt
response = model.generate_content("Convert this text to XML: [Your text here]")
# Use the output
print(response.text)
2. Subscription vs. Pay-As-You-Go
Most AI APIs do not use monthly subscriptions like ChatGPT Plus. Instead, they use a Pay-As-You-Go model.
- Free Tiers: Both Google (Gemini) and OpenAI offer "Free Tiers" for API usage, but they have strict limits on how many requests you can make per minute.
- Billing: Once you exceed the free tier, you are charged based on tokens (roughly 0.75 words per token). If you are processing 100 articles, the cost is usually negligible (often just pennies).
- Adding Credit: You typically need to add a credit card to your developer account. The system will then deduct fractions of a cent as you make API calls.
3. Constraints (The Technical "Gotchas")
When building an automated pipeline, you will encounter these three main constraints:
- Rate Limits (RPM/TPM): * RPM (Requests Per Minute): How many times you can ask the AI a question per minute.
- TPM (Tokens Per Minute): How much total text you can send in one minute.
- Solution: If you hit these, your script must "sleep" for a few seconds before trying again.
- Context Window: Every model has a maximum "memory" (context window). Even if you want to process a large file, you cannot send a 500-page book in one request. You must break the document into the "chunks" we discussed earlier.
- Output Consistency: AI can occasionally "hallucinate" or drift from your requested XML schema.
- Solution: Always include your schema in the prompt (as a "system instruction") and use a library like lxml or xmllint in your Python script to validate the XML before you save it to your file.
Visualizing the Pipeline
The interaction between your local machine and the cloud API is a direct request-response cycle.
Suggested Next Steps
- Register: Go to Google AI Studio, get a free API key, and try the sample code above.
- Test: Create a Python script that processes just one article first to verify the XML output is exactly what you want.
- Scale: Once the single-article script works, wrap it in a loop to process your entire document.
Do you have an API key ready, or would you like me to walk you through the steps to set up your environment on your computer first?
UP: How many pages of text will i be able to process for 1 dollar
Response: The amount of text you can process for $1 depends on which model you choose and whether you are paying for the input (the documents you provide) or the output (the XML the AI generates).
As of 2026, using Gemini 1.5 Flash (a highly efficient model for high-volume tasks), here is a breakdown to help you estimate your costs.
1. The Math
- Tokens per Page: A standard page of single-spaced text is approximately 500 to 750 words. In "token" terms, this usually equals about 750 to 1,000 tokens.
- Pricing: Gemini 1.5 Flash input costs roughly $0.10 to $0.45 per 1 million tokens (depending on the specific tier/version). Let’s use a conservative estimate of $0.25 per 1M tokens to account for a mix of input and output.
2. How much is $1?
If we assume $0.25 per 1,000,000 tokens:
- $1 buys you roughly 4,000,000 tokens.
- At 1,000 tokens per page, that is 4,000 pages of text.
| Processing Task | Estimated Volume for $1 |
|---|---|
| Input (Reading your files) | ~4,000 – 5,000 pages |
| Output (Generating XML) | ~1,500 – 2,000 pages |
| Combined (Typical Workflow) | ~1,200 – 1,800 pages |
Key Factors That Change This Number
- Model Choice: If you switch to "Pro" models (more "intelligent" but more expensive), your cost per page will increase by 5x to 10x, significantly reducing how many pages you get for $1.
- Output Length: If your XML annotations are very verbose (lots of metadata tags), you are paying for "output tokens," which are generally more expensive than "input tokens."
- Context Caching: If you are processing the same encyclopedic documents repeatedly, you can use Context Caching to significantly lower costs after the first read.
The Bottom Line
For your project of 100 articles, you will likely spend less than $1 to process the entire set, provided you use the Flash model and standard XML formatting.
Recommendation: Start by setting up your Google AI Studio account and processing one file. The "Free Tier" in AI Studio is often generous enough that you might be able to process your entire 100-article library without even needing to enter a credit card initially.
Would you like to move forward with writing the Python script to run this test?
Sources
- Gemini API Pricing Calculator & Cost Guide (May 2026) - CostGoat
- Gemini CLI Pricing Guide - Complete Cost Analysis 2025
UP: Ok.lets start
Response: That is the right spirit! Since we are ready to move from planning to action, let's establish a clear, iterative process for your first test run.
To ensure your first experiment is a success, we will start by creating a "Test Suite". This consists of:
- One file containing a few sample articles.
- A Python script that reads the file, calls the API, and saves the formatted output.
Phase 1: Preparation (The Toolkit)
Before we write the code, ensure you have these ready:
- API Key: Get your key from Google AI Studio.
- Environment: Ensure you have Python installed and run pip install -U google-generativeai in your terminal.
- Test Data: Save 2-3 of your encyclopedic articles into a plain text file named test_articles.txt.
Phase 2: The Script Template
This script uses the "Chunking" method we discussed. It reads your text and sends it to the AI to be processed.
import google.generativeai as genai
# Configure your API key
genai.configure(api_key="YOUR_API_KEY_HERE")
model = genai.GenerativeModel('gemini-1.5-flash')
def process_chunk(text_chunk):
prompt = f"""
You are an expert editor for MediaWiki.
Convert the provided encyclopedic text into MediaWiki Export XML.
- Wrap the content in <page> tags.
- Use CDATA for the content.
- Keep formatting: '''bold''', ''italic'', <sub>sub</sub>, <sup>super</sup>, ==Section==.
- If multiple articles are found, wrap each in its own <page> tag.
Text to process:
{text_chunk}
"""
response = model.generate_content(prompt)
return response.text
# Process your test file
with open('test_articles.txt', 'r', encoding='utf-8') as f:
text = f.read()
xml_output = process_chunk(text)
# Save the result
with open('output_test.xml', 'w', encoding='utf-8') as out:
out.write('<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/">\n')
out.write(xml_output)
out.write('\n</mediawiki>')
Phase 3: The "Sanity Check"
Before we scale this to your full library of 100 articles, here is how we will evaluate the output:
- XML Validity: Does the XML file open in a browser? If it shows a syntax error, we adjust the CDATA handling or the escaping logic.
- Markup Fidelity: Does the AI correctly identify the bolding and section headers?
- Segmentation: Did the AI identify all the articles in your test file, or did it miss one?
Ready to proceed? If you have your API key and Python environment set up, go ahead and create your test_articles.txt file.
Would you like me to help you refine the regex logic for a more robust segmentation, or shall we run this first test and see how the AI handles your specific document structure?
UP: The second
Response: To move forward with the "Developer Route" (using Python to automate your transformation), we need to ensure the structure you are generating is valid for MediaWiki's importDump.php tool.
Below is the refined plan to build your automation engine.
1. The Core XML Template
MediaWiki export files expect a specific structure. Since you are using CDATA, you have the advantage of keeping your source formatting (Wikitext) clean inside the XML.
Your script needs to wrap every article entry into a page block like this:
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/">
<page>
<title>Article Title</title>
<revision>
<text xml:space="preserve" bytes="1234"><![CDATA[
== Section Title ==
Content with '''bold''' and ''italic'' text.
]]></text>
</revision>
</page>
</mediawiki>
2. The Python Pipeline Strategy
We will use a modular approach. This script will handle the I/O, while the Gemini API acts as the intelligence layer for segmentation and formatting.
Your technical roadmap:
- Loader: Read your large source file.
- Orchestrator: Send chunks to Gemini with a "Few-Shot" prompt containing your schema requirements.
- Generator: Assemble the returned <page> blocks into the final valid XML file.
3. Recommended Python Setup
Before writing the code, ensure you have your environment ready:
- Python: Install the Google Generative AI library: pip install google-generativeai.
- API Access: Use your Google AI Studio API Key.
- Validation Tool: Install libxml2-utils (Linux/macOS) so you can run xmllint to verify your generated XML file.
Step-by-Step for the First Test
To start, we should process a single, small file. This validates that the AI understands how to segment your 100 articles and format them according to the MediaWiki schema.
I have drafted the logic below. Shall we proceed to the full implementation of this script so you can test it on your first file? (Note: Once we have the script, you'll be able to process your 100-article file in one execution.)
UP: Can u summarise me the conversation we had with you till this point?
Response: We have been developing a strategy to automate the transformation of a large volume of unstructured encyclopedic text files into a MediaWiki Export XML format. Here is a summary of the journey so far:
- Objective: You need to process roughly 100 encyclopedic articles contained within larger documents, ensuring they are properly formatted with Wikitext (bold, italics, headers, sub/superscripts) and compliant with the MediaWiki XML schema.
- The Technical Approach: We established that manual processing is inefficient. Instead, we are building a Python-based pipeline that leverages the Gemini API.
- Workflow Strategy:
- Segmentation: Rather than you manually splitting the files, we decided the AI itself is the best "segmenter." You will feed the text in "chunks" to the API, and the AI will identify individual articles and generate the corresponding XML.
- Data Formatting: We opted to use CDATA sections (<![CDATA[...]]>) within the XML tags. This allows us to keep your formatting (Wikitext and HTML) intact without the errors caused by manual character escaping.
- Automation: We discussed using a Python script to iterate through your files, utilizing the Gemini 1.5 Flash model for a cost-effective, scalable solution.
- Learnings: You learned that the AI can "learn" your desired output structure through Few-Shot Prompting (providing examples of input and desired output), and that this is more practical than model fine-tuning for your specific use case.
- Next Steps: We are ready to implement a test script using your API key to process a small batch of articles to verify the XML integrity and formatting before scaling up to your full library.
We have now moved from the planning phase to the "execution" phase, where the next step is to write and run a Python script to perform the actual conversion.
Document Understanding with Gemini AI and Python
This video is relevant because it provides a practical, step-by-step tutorial on using Python and the Gemini API to extract and process structured information from documents, which is exactly the skill set required to build your automation pipeline.
http://googleusercontent.com/youtube_content/0
Sources
- Document Understanding with Gemini AI and Python | PDF Information Extraction & Summarization - YouTube
Google AI Studio
UP: Would it be appropriate to use Google AI Studio for my project
Response: Yes, Google AI Studio is highly appropriate for your project. In fact, it is arguably the best place to start.
Here is why it is perfectly suited for your specific task:
1. It is the "Lab" for your API
Since we have decided to use the Gemini API to process your documents, AI Studio is where you will:
- Test your prompts: You can experiment with different instructions (System Prompts) to see how well the AI segments your articles before you write a single line of code.
- Fine-tune the output: You can instantly see the generated XML and verify it matches your MediaWiki schema.
- Get your API Key: It is the central hub where you generate the API key required for your Python script to communicate with Google's models.
2. Rapid Prototyping (Few-Shot Testing)
You mentioned wanting the AI to learn how to segment and annotate. AI Studio has a specific "Prompt" interface that makes it incredibly easy to add your "Golden Examples." You can paste 3-5 examples of your input text and the corresponding XML output into the prompt interface, and test immediately how the model performs on a new piece of text.
3. Cost and Access
- Free Tier: As discussed, AI Studio offers a generous free tier. For a project of 100 articles, you may be able to complete the entire conversion process without paying anything.
- No Installation Required: It runs entirely in your browser, meaning you don't need to configure complex development environments just to test your logic.
How to use it for your current goal:
- Go to aistudio.google.com.
- Create a new "Chat Prompt."
- Paste your "System Instructions": Define the role (e.g., "You are an expert at converting encyclopedic content to MediaWiki XML").
- Add your Examples: Use the chat interface to show the AI: "Here is raw input X, here is desired output Y."
- Test: Paste a chunk of your real document and see if it segments and tags the content correctly.
- "Get Code": Once you are happy with how the AI is performing, click the "Get Code" button in the top right corner. It will give you the exact Python snippet needed to perform that exact task in your local script.
By using AI Studio, you bypass the "guesswork" of prompting. You can iterate in the browser until you get perfect results, then export that logic directly into your Python script.
Does this seem like a comfortable starting point, or would you like me to help you draft the specific "System Instructions" you should paste into AI Studio to begin testing?
Sources
- Firebase Studio sunset and project migration - Google
- Google AI Studio: A Deep Dive for Businesses & Builders (2025)
- Can somebody explain Google AI Studio for me? : r/Bard - Reddit
- AI for developers | Google Cloud
- Google AI Studio - Wikipedia
- My Experience with Google AI Studio: A Hidden Gem for Content Creators
- Google AI Studio quickstart | Gemini API
UP: Is there mobile version of Google AI Studio
Response: Yes, Google has recently launched native mobile apps for Google AI Studio on both Android and iOS.
Key Details:
- Native Apps: As of May 2026, you can download the official Google AI Studio app from the Google Play Store and the Apple App Store. These apps are designed to provide a tactile, "vibe coding" experience, allowing you to build apps, experiment with prompts, and generate ideas directly from your smartphone.
- Seamless Handoff: One of the primary benefits of the mobile app is the ability to start a prototype or idea on your phone while on the go and then easily transition to your computer to refine it or "go deep" into the development flow when you return to your desk.
- Browser Access: You can still access the platform via a mobile web browser at aistudio.google.com, which also supports the full suite of features—including building and testing Android apps directly in the browser—though the native apps provide a more optimized mobile experience.
If you are planning to use this for your project, the mobile app is great for quick ideation and testing, while the desktop environment remains the standard for the more complex coding and file-handling tasks involved in your MediaWiki conversion project.
Sources
- Google AI Studio adds a mobile app to let users build AI projects on smartphones without traditional software complexity after eight weeks of development - Digg
- Google AI Studio - Apps on Google Play
- Bring any idea to life: Google AI Studio at I/O 2026
UP: after I create a code in Google AI Studio can I place it into Co-op
Response: You are likely thinking of Google Colab (often shortened to "Colab"), not "Co-op."
Yes, Google AI Studio has a built-in feature to export your work directly into Google Colab, which makes it very easy to transition from prototyping your prompt/logic to running and testing the actual Python code.
How to use the "Open in Colab" feature:
- In Google AI Studio: After you have refined your prompt and are happy with the logic, look for the "Get Code" button (usually at the top right of the prompt editor).
- Select Colab: When you click that button, you will see a tab for "Colab" (or an option to "Open in Colab").
- Automatic Launch: This will automatically open a new Google Colab notebook in your browser, pre-populated with the necessary code snippets, API setup, and the logic you just built in AI Studio.
Why this workflow is perfect for your project:
- Zero Configuration: Google Colab is a "hosted" environment. You don't need to install Python, libraries, or manage paths on your local computer to test the code.
- Immediate Execution: You can run the code cell by cell to see if the XML conversion works as expected, and you can immediately see the output generated by the Gemini API.
- File Handling: Colab connects easily to your Google Drive, so if your 100-article file is stored there, you can mount your drive, read the file, and process it entirely within the Colab environment.
A quick note on "Co-op"
There is no major modern coding platform called "Co-op" used for this type of AI development. You may have seen references to "Code Co-op" (a legacy version control system) or "Platform Cooperativism" in older tech articles, but in the context of Google AI Studio, Google Colab is definitely the environment you are looking for.
Are you ready to try exporting your code, or would you like to refine the logic for your article-segmentation script first?
Sources
- Google AI Studio - Open code in Google Colab - Studyopedia
- Colab | Google for Developers
- Welcome To Colab - Colab - Google
UP: yes I would like to refine the logic for my article segmentation script adding encoding image and references
Response: To refine your article segmentation and formatting pipeline, we need to address three pillars: Robust Encoding, Reliable Segmentation, and Structured Output.
1. Robust File Encoding (The Foundation)
You asked about encoding because inconsistent character sets can crash an automated pipeline (e.g., encountering a symbol that isn't UTF-8).
Best Practice: Always explicitly declare encoding='utf-8' when reading or writing files in Python. If your source files are messy (perhaps old StarDict imports), use the errors='replace' or errors='ignore' arguments to prevent the script from stopping entirely if it hits a single malformed character.
# The "Bulletproof" way to open files
with open('data.txt', 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
2. Intelligent Segmentation (The AI-Driven Logic)
Since you don't want to manually split the articles, we will use a sliding window approach. We feed the AI large chunks of text and ask it to return a list of identified articles.
3. The Refined Python Script
This script combines the encoding safety, the chunk-based segmentation, and the MediaWiki XML structure with CDATA.
import google.generativeai as genai
# API Configuration
genai.configure(api_key="YOUR_API_KEY")
model = genai.GenerativeModel('gemini-1.5-flash')
def process_chunk_to_xml(chunk):
prompt = f"""
You are a data conversion engine. Analyze the text below and identify all encyclopedic articles.
For each article:
1. Extract the Title and Content.
2. Format as a MediaWiki <page> block.
3. Use <![CDATA[ ]]> for the <text> element.
4. Maintain all Wikitext (==headers==, '''bold''', ''italic'', <sub>, <sup>).
Text chunk:
{chunk}
"""
response = model.generate_content(prompt)
return response.text
# Process your file safely
try:
with open('articles.txt', 'r', encoding='utf-8', errors='replace') as f:
# Process in 8,000-character chunks
while True:
chunk = f.read(8000)
if not chunk: break
xml_fragment = process_chunk_to_xml(chunk)
# Append result to your final output
with open('final_export.xml', 'a', encoding='utf-8') as out:
out.write(xml_fragment + "\n")
except Exception as e:
print(f"Error occurred: {e}")
References for your Learning Path
- MediaWiki Export Schema: Understand the required XML structure you are targeting.
- Python codecs Module: Deep dive into how Python handles character encoding and decoding if you run into specific legacy formatting issues.
- Google Gen AI SDK Documentation: Keep this bookmarked to stay updated on the latest gemini-1.5-flash features and best practices for prompt engineering.
Next Step
Before running the full script, create a test_articles.txt with exactly 3 articles. Run the script above and check final_export.xml. If the AI correctly identifies the 3 articles and wraps them in <page> tags, you are ready to process all 100 articles.
Shall I help you draft the final "System Instruction" for the AI that ensures it NEVER forgets to close the CDATA or <page> tags?
UP: Ok. And reiterate where should I put sample output results
Response: To ensure your AI stays on track and produces perfect XML every time, you should place your "sample output results" in three distinct locations, depending on where you are in your development cycle.
Here is the strategy for where to place your examples:
1. In Google AI Studio (The "Prototyping" Location)
When you are refining your prompt, this is the most critical place for your examples. You should put them in the "Few-Shot" section of the prompt.
- Where: In the AI Studio interface, use the "Few-Shot" or "Examples" section.
- Why: The AI uses these as a template. By providing an Input: [Raw Text] and an Output: [XML] pair, you create a "ground truth" that the model will mimic for every new chunk you feed it.
- Format: User: Convert this: "==Apple== An apple is a fruit." Model: <page><title>Apple</title><revision><text xml:space="preserve"><![CDATA[==Apple== An apple is a fruit.]]></text></revision></page>
2. In your Python System Prompt (The "Production" Location)
Once you have tested in AI Studio and are ready to use your script, you should bake your best example directly into the prompt string inside your Python code.
- Where: Inside your process_chunk_to_xml function.
- Why: This ensures that every time your script sends a request, the AI is "reminded" of the exact format you require, even if it has drifted during long sessions.
- Format: prompt = f""" You are an XML conversion engine. Follow this format exactly: Example Input: "==Title== This is text." Example Output: <page> <title>Title</title> <revision> <text xml:space="preserve"><![CDATA[==Title== This is text.]]></text> </revision> </page> Now convert this: {chunk} """
3. In a "Schema Validation" File (The "Safety" Location)
You should keep a local file (e.g., golden_schema.xml) on your computer that contains the "perfect" output you expect.
- Where: A text file in your project folder.
- Why: Use this to compare against your actual output. If the AI starts producing malformed XML, you can run a simple "diff" (comparison) between your golden_schema.xml and your generated output to see exactly where the formatting broke.
Summary Table: Where to put what?
| Location | Purpose | Best for... |
|---|---|---|
| AI Studio (Few-Shot) | Prototyping | Testing the AI's "understanding" of your formatting. |
| Python Script (Prompt) | Automation | Maintaining consistency across all 100+ articles. |
| Golden Schema File | Validation | Comparing "what I want" vs. "what I got." |
A final tip on your "System Instruction"
When you put these examples into your prompt, add this Golden Rule at the end:
"If the input contains multiple articles, produce a list of <page> blocks, one for each article, without any extra text or conversational filler."
This prevents the AI from saying things like "Here is your XML output:" at the start of the file, which would break the importDump.php tool in MediaWiki.
Does this help you understand how to organize your workflow, or do you need help writing the specific text for your "System Instruction"?
UP: Structured outputs Enter an OpenAPI schema object to constrain the model output. See the API documentation for examples. Should I enter my MediaWiki Export XML output schema here
Response: The short answer is no, do not use the "Structured Outputs" schema feature for MediaWiki XML.
Here is the explanation why, and what you should do instead.
1. Why "Structured Outputs" is not for this
The "Structured Output" feature in Google AI Studio and the Gemini API is designed specifically to constrain the model to output JSON (JavaScript Object Notation).
- It requires JSON: It is built to guarantee that the output is valid JSON according to a specific JSON schema.
- MediaWiki is XML: Since you need XML (specifically the MediaWiki Export format), trying to force this feature to produce XML will likely result in a system error or a failure to comply, because the underlying mechanism expects JSON keys and values.
2. The Right Way to Constrain XML
For MediaWiki XML, you are not trying to constrain "data types" (like integers or booleans); you are trying to constrain "document structure" and "formatting."
The best way to force the AI to produce perfect MediaWiki XML is Prompt-Based Schema Enforcement. This is what we have been planning all along:
- Use the System Instructions: Clearly define that the output must be valid XML that conforms to the MediaWiki export-0.10 schema.
- Use "Few-Shot" Examples: In your prompt, provide the AI with a "Golden Sample" of exactly one <page> block.
- Use the CDATA wrapper: As we discussed, wrapping the content in <![CDATA[ ... ]]> is the industry-standard way to "lock" the inner content so the AI doesn't try to "edit" or "reformat" your XML structure, ensuring the output remains valid.
3. What to do instead in AI Studio
Inside Google AI Studio, instead of using the "Structured Output" configuration:
- System Instructions: Put your schema requirements here (e.g., "Always use xml:space="preserve" and valid MediaWiki export-0.10 XML tags").
- Prompt Area: Paste your "Few-Shot" examples (the Input text + The corresponding XML output).
- Test: Feed it a new article and check if the output validates.
If you really want automated validation...
If you are worried about the AI producing broken XML, the correct "developer" way to handle this is Post-Generation Validation in your Python script:
import lxml.etree as ET
# 1. Get the AI's output
output = model.generate_content("...")
# 2. Use Python to check if it's actually valid XML
try:
ET.fromstring(output.text)
print("XML is valid!")
except ET.XMLSyntaxError:
print("AI produced broken XML. Time to re-prompt or retry.")
Recommendation: Forget about the "OpenAPI schema" box. It is a powerful tool for JSON data extraction, but it is the wrong tool for generating complex, tag-heavy XML documents. Focus on perfecting your System Instructions and Few-Shot examples—that is where you will get the best results for your MediaWiki project.
Does this make the distinction clearer, or would you like to see how to write a Python script that automatically validates the XML the AI returns?
Sources
- Generate structured output (like JSON and enums) using the Gemini API | Firebase AI Logic
- Structured output | Gemini Enterprise Agent Platform - Google Cloud Documentation
UP: Will Google AI Studio generate python code for me?
Response: Yes, Google AI Studio is an excellent tool for generating Python code, and it provides specific features to make this workflow seamless.
How it works
There are two main ways Google AI Studio helps you with Python:
- Direct Code Generation: You can ask the AI in the chat interface to write Python functions, algorithms, or scripts, and it will generate the code directly in the response.
- The "Get Code" Feature: This is the most powerful tool for developers. Once you have a prompt, system instruction, or configuration that works exactly how you want, you can click the "Get Code" button (located in the top right corner). This instantly provides you with a ready-to-use Python snippet (using the Gemini API) that implements your current prompt logic.
Why it is great for your project
Since we are building a MediaWiki processing pipeline, AI Studio is ideal because:
- Prototyping: You can test your segmentation and XML-formatting prompts in the AI Studio playground until they are perfect.
- Instant Export: Once the logic is perfect, you don't have to manually rewrite it for Python. You export the working code directly to your environment or to Google Colab (which is perfect for testing your "chunk-based" processing without needing a complex local setup).
- Code Execution: The Gemini API also supports a "Code Execution" tool, meaning the model can generate and run Python code internally to process data or solve problems, which can be useful if you need the AI to verify its own logic.
Quick Workflow Tip
When you use the "Get Code" button, the AI will provide a starter implementation that uses the google-genai library. You will just need to:
- Ensure you have the library installed (pip install google-genai).
- Paste your API key into the script.
- Adjust the file paths or variables to point to your specific documents.
It essentially turns your "prompting" into "programming" automatically.
How to Get Code with Google AI Studio (2026 Full Guide)
This video provides a practical, visual guide on how to use the "Get Code" feature in Google AI Studio to turn your AI prompts into functional, ready-to-run Python code.
Sources
- Google AI Studio: The Playground Every Developer Should Know About - DEV Community
- Code execution - generateContent API | Google AI for Developers
- Building an AI Chatbot with Google AI Studio & Flask. And how you can create one yourself.
UP: .give me inclusive generic definition of Google AI Studio
Response: Google AI Studio is a web-based integrated development environment (IDE) designed to provide users with direct access to Google’s latest generative AI models, particularly the Gemini family. It serves as a bridge between high-level experimentation and practical application development.
You can think of it as a professional "AI workshop" where you can prototype, refine, and deploy AI-driven solutions without needing a complex local setup.
Core Functions and Capabilities
- Prototyping and Experimentation: It allows you to test prompts, adjust model parameters (such as temperature, safety settings, and output length), and observe how different models respond to your specific use cases.
- Multimodal Processing: The platform supports more than just text; it can process and analyze various media, including images, video, audio, and large document files.
- Prompt Engineering: You can use multiple prompting techniques, such as zero-shot (giving the AI a task with no examples) or few-shot (providing the AI with several input/output pairs to "teach" it a desired pattern or structure).
- Code Integration: Once you have refined your prompt or application logic in the studio, the platform provides a "Get Code" feature that generates the necessary Python, JavaScript, or other language-specific code snippets to integrate that logic directly into your own software projects via the Gemini API.
Who is it for?
- Developers: Those looking to build AI-powered apps or integrate AI features into existing services. It offers API keys and documentation to facilitate this integration.
- Creatives and Hobbyists: Individuals interested in exploring the capabilities of advanced models for creative tasks like image generation, content drafting, or even building interactive apps.
- Non-technical Users: Because it operates as an intuitive, browser-based interface, users without formal coding skills can still use it to test prompts and explore the power of generative AI.
In essence, while general-purpose chatbots (like the standard Gemini web app) are designed for conversational interaction, Google AI Studio is designed for building. It provides the tools and environments necessary to transform an idea into a functional AI application.
Sources
- Google AI Studio - Wikipedia
- Can somebody explain Google AI Studio for me? : r/Bard - Reddit
- Google AI Studio quickstart | Gemini API
- My Experience with Google AI Studio: A Hidden Gem for Content Creators
- Google AI Studio
- What is Google AI Studio and how to use it in 2026 | by Chandler K - Medium
- Bring any idea to life: Google AI Studio at I/O 2026
UP: What languages can Google AI Studio generate?
Response: Google AI Studio operates on two different levels regarding languages: what the AI can write and what the API supports for integration.
1. Code Generation (What the AI can write)
Because the underlying Gemini models have been trained on vast amounts of public-domain code, Google AI Studio can generate code in virtually any major programming language. You are not limited to a specific list.
- Popular Languages: It is highly proficient in Python, JavaScript/TypeScript, Java, C++, C#, Go, Ruby, PHP, Swift, Kotlin, Rust, and SQL.
- Web & Scripting: It handles HTML, CSS, Bash/Shell scripts, PowerShell, and various configuration formats (JSON, YAML, XML, Dockerfiles) with ease.
- Frameworks: Beyond raw syntax, it understands modern frameworks like React, Node.js, Jetpack Compose, Flutter, and others.
If you describe a function or an entire application architecture in plain English, the AI can translate that into the syntax of almost any common modern language.
2. API Support (What you can use to build your own apps)
If you are using the "Get Code" feature or integrating the Gemini API into your own software, Google provides official SDKs (Software Development Kits) for specific languages. These are the languages Google officially maintains to help you talk to their AI services:
- Python (google-genai)
- JavaScript/TypeScript (@google/genai)
- Go (google.golang.org/genai)
- Java (google-genai)
- C# / .NET (Google.GenAI)
3. Special Case: "Vibe Coding" & Build Mode
Google AI Studio recently introduced "Build Mode" (Vibe Coding), which changes the dynamic significantly.
- Zero-Syntax Workflow: In this mode, you don't need to choose a language. You describe what you want to build (e.g., "a budget tracker"), and the studio generates the necessary full-stack environment—typically using React (JavaScript) for the frontend and Node.js (JavaScript) for the backend—automatically handling the setup, packages, and integration for you.
Summary
- If you want the AI to write a snippet for a project: It can write in almost any language you name.
- If you are building an application that calls the Gemini API: You should use the officially supported SDKs (Python, JavaScript, Go, Java, or C#).
- If you are using "Build Mode": The environment is managed for you, predominantly using the JavaScript ecosystem.
Are you looking to use the API in a specific language for your MediaWiki project, or were you wondering if the AI could handle a more obscure language?
Sources
- Google AI Studio: Build, Generate & Deploy AI Models
- Supported languages, IDEs, and interfaces | Gemini for Google Cloud
- Getting started with Google AI Studio: from idea to functional web app in minutes
- Build apps in Google AI Studio | Gemini API
- Vibe Coding | Google AI Studio
- Gemini API libraries - Google AI for Developers
UP: Is Google AI Studio able to access Google Drive files
Response: Google AI Studio does not have a direct, native "browse my Google Drive" button inside the prompt interface like the standard Gemini web chat does.
Because it is a developer tool designed for building applications, it handles files differently. Here is how you should handle your Drive files for your MediaWiki project:
1. The "Manual Export" Approach (Simplest)
For your project of 100 articles, you do not need a complex integration.
- Download: Go to your Google Drive, select your documents, and download them to your local computer (e.g., as .txt or .pdf files).
- Upload: In Google AI Studio, use the attachment icon (or the "plus" sign) in the prompt area to upload these files directly.
- Context: Once uploaded, the Gemini model can read them as context for your prompt. Note that these files are temporary and exist for the session.
2. The "Developer" Approach (Automated)
If you are building a script to automate this (as we planned in our previous steps), you don't "connect" AI Studio to Drive; you use Python to bridge the two.
- Workflow: Your Python script will use the google-api-python-client or similar libraries to fetch the content from Drive, and then pass that content into the genai.generate_content call to the Gemini API.
- Why this is better: It allows you to loop through all 100 articles automatically without you having to manually upload each one.
3. Using Third-Party Automators (No-Code)
If you prefer not to write the file-fetching logic yourself, you can use automation platforms like Zapier or Make.
- How it works: You set a "Trigger" (e.g., "New file in this Drive folder") and an "Action" (e.g., "Send content to Gemini API").
- When to use this: Use this only if you want the process to happen automatically every time you drop a new file into a specific folder, rather than running a one-time Python script.
Important Distinction
- Gemini (gemini.google.com): This is the consumer chatbot. It can search your Drive directly if you have the right permissions enabled.
- Google AI Studio (aistudio.google.com): This is the developer environment. It is optimized for API integration rather than direct user-data access.
My recommendation for your project: Since you are already planning to use a Python script, stick to the Developer Approach. Write a small Python function that reads your files from a local directory (where you’ve downloaded them from Drive). It is much more stable, easier to debug, and costs you nothing in third-party automation fees.
Would you like me to show you the Python snippet that lists and reads all files in a folder so you can start processing your 100 articles?
Sources
- How to Connect Google Drive to Google Gemini (4 Methods) | Context Link
- Files API - generateContent API - Google AI for Developers
- Google AI Studio File Management: The 2025 Guide | Fastio
- Google Drive Google AI Studio (Gemini) Integration - Quick Connect - Zapier
- Collaborate with Gemini in Google Drive (Workspace Experiments) - Android
- Bring any idea to life: Google AI Studio at I/O 2026
UP: Which format is better to feed pdf or docx
Response: For your MediaWiki project, DOCX (or even better, Markdown or Plain Text) is significantly better than PDF.
Here is the breakdown of why you should avoid PDF for this specific task and what you should use instead.
1. Why PDF is "Tricky" for AI
Even though Gemini is excellent at "seeing" PDFs, using them for data conversion introduces unnecessary layers of complexity:
- The "Invisible" Processing: When you upload a PDF, the API often invokes an internal OCR or layout-parsing engine. This adds latency and introduces a chance for "artifacts"—random character errors or strange line breaks—that can break your XML structure.
- Layout Distraction: PDFs are designed for visual presentation (how a page looks), not data structure (what the content is). The AI may get distracted by headers, footers, page numbers, or column layouts, which can confuse the segmentation of your articles.
- Token Inefficiency: Because PDFs are treated as images or complex layouts, they can be "heavier" to process, consuming more resources than a clean text-based file.
2. The Hierarchy of "Best Formats"
If you have the choice, use these formats in this order of preference:
| Priority | Format | Why it's best |
|---|---|---|
| 1 (Best) | Markdown (.md) | It natively uses headers (#), bold (**), and lists. This mirrors MediaWiki syntax, making the AI's job trivial. |
| 2 | Plain Text (.txt) | No hidden formatting to trip up the AI. It is 100% data. |
| 3 | DOCX | Structured well, but requires the API to "unpack" the file structure, which is slightly more overhead than TXT. |
| 4 (Avoid) | Heavy, visual-first, prone to extraction artifacts. |
3. Recommendation for your 100-Article Project
Since your goal is to produce MediaWiki XML, you should try to convert your source files to Plain Text or Markdown before feeding them to the script.
How to do this quickly:
- If your files are in Google Drive, you can "Download as" -> "Plain Text (.txt)" directly from Google Docs.
- Once they are in .txt or .md format, your Python script will be much faster, cheaper to run, and the output XML will be significantly more accurate because the AI won't have to "guess" the formatting from a PDF scan.
The Golden Rule for your pipeline: The more you "pre-clean" your data (by removing unnecessary formatting, page numbers, and decorative elements), the less "intelligence" the AI has to waste on cleaning, and the more "intelligence" it can dedicate to perfect XML tagging.
Does that change how you plan to handle your source files, or are they already saved in a specific format?
UP: I have docx file. Article names are given in all capital and mainly bold font. Is it enough for Google AI Studio models to recognise boundaries?
Response: The short answer is: It is a great start, but it is not "guaranteed" enough for a 100% accurate automated pipeline.
Gemini is very good at understanding visual cues like BOLD ALL CAPS, but relying on them as the only signal for segmentation can lead to errors (e.g., if a sub-section header or a citation in the text happens to be in bold caps as well).
To make your segmentation rock-solid, you should combine those visual cues with "Few-Shot" instructions. Here is how to make the model "see" what you see.
1. The Strategy: "Anchor" the AI
Instead of just asking the AI to "find the articles," you should define the "Anchor Pattern" in your system prompt.
Example System Instruction:
"You are an expert document parser. An article begins when you encounter a line that is BOLD and in ALL CAPS. This line serves as the <title> of the article. Everything following this title until the next instance of a BOLD ALL CAPS line belongs to that article's content."
2. Why this is safer than "just hoping"
If you don't give this instruction, the AI might accidentally treat a sub-heading or an emphasized word in the middle of a paragraph as a new article. By explicitly defining the boundary, you force the AI to use its "logic" layer to confirm the visual cue.
3. Improving the DOCX Source
If you are worried about the AI missing boundaries, you have two simple ways to "pre-process" your DOCX before you even upload it to AI Studio:
- The "Style" Trick (Best for DOCX): If your bold/all-caps titles are actually marked as "Heading 1" or "Heading 2" in Microsoft Word, the AI can "read" the structural metadata of the DOCX much more reliably than it can read "boldness." If you can, quickly apply a Heading style to those titles in Word.
- The "Cleartext" Conversion: As discussed, converting your DOCX to Markdown (.md) is the absolute safest bet. In Markdown, your titles become # ARTICLE TITLE. The AI is 100% perfect at recognizing that # marks a new section boundary.
Recommended Workflow for your 100 Articles:
- Convert to Markdown: If your DOCX is simple, open it in Word/Google Docs and "Save As" / "Download" as .txt or .md.
- Standardize: If the titles are all caps, perform a "Find and Replace" in your text editor. Replace the bolded caps with a Markdown Header (e.g., # TITLE).
- Prompt the AI: Now, your system instruction becomes even simpler and more accurate: "Extract content between # headers."
Does your DOCX contain a lot of "false positives" (e.g., words in all caps that AREN'T titles), or is the formatting very consistent throughout the 100 articles?
UP: I need to state my content source docx files created as an output of OCR recognition. So there is no specific styles
Response: Since you are working with OCR-generated text, you face a unique set of challenges: the formatting is unreliable, and you will likely have "hallucinated" characters (e.g., O vs 0, l vs 1) or missing line breaks.
Relying on "all caps and bold" as a primary boundary in raw OCR text is dangerous because OCR engines often struggle with font detection, meaning a paragraph header might not be properly "bolded" in the digital output.
Here is how to handle OCR-based source files for your project:
1. The Strategy: "Pattern Recognition" over "Style Recognition"
Instead of looking for styles (which aren't there), you must instruct Gemini to look for semantic patterns. Since encyclopedic articles have predictable structures, you can use these as your "Anchors":
- The "Title" Anchor: Even if it isn't bold, an article title almost always appears at the start of a line and is typically short (under 5–10 words).
- The "Date/Metadata" Anchor: Does every article start with a specific format? (e.g., [Year] or Name, (Birth-Death)).
- The "Empty Line" Anchor: OCR output often leaves an empty line between entries. You can tell the AI: "Treat every block of text separated by double newlines (\n\n) as a potential new entry."
2. The "Pre-Processing" Step (Crucial)
Before feeding the files to the AI, use a small Python function to "clean" the raw OCR output. This will save you massive amounts of money and improve accuracy:
import re
def clean_ocr_text(text):
# 1. Remove common OCR noise (e.g., repeating underscores, page headers)
text = re.sub(r'_{3,}', '', text)
# 2. Fix erratic line breaks (common in OCR)
# This replaces single newlines with spaces, but keeps double newlines
text = re.sub(r'(?<!\n)\n(?!\n)', ' ', text)
return text
# Use this before sending the text to the API
3. Updated System Instruction for OCR Data
Because your data is "noisy," your system prompt needs to be more protective. Use this style:
"You are an expert Data Recovery Specialist. You are receiving raw, unformatted OCR text.
- Your task is to identify individual encyclopedic articles.
- Boundary Rule: An article starts with an All-Caps Title (even if the OCR failed to detect bolding). If a line is short and in all caps, treat it as a title.
- Noise Handling: If you encounter gibberish, symbols, or fragmented sentences that clearly result from OCR scanning errors, fix them contextually if possible, or ignore them if they are clearly 'junk' artifacts (like page numbers or headers).
- Output: Output only the MediaWiki XML. Do not include introductory text."
4. Recommendation: The "Validation" Loop
Since OCR is imperfect, the AI might accidentally merge two articles or cut one in half.
- Run a Test: Run 5 pages through your script.
- Spot Check: Open the generated XML. If you see two <title> tags inside one <page> tag, you know the AI is struggling to find the boundaries.
- Tighten the Prompt: If it fails, add a "Negative Constraint": "Do not include more than one article per <page> block."
Would you like me to help you write a more advanced regex-based pre-processor to clean up your OCR files before they even reach the AI? (This will significantly lower your costs by reducing the "work" the AI has to do on each request.)
Sources
- Image Enhancement and OCR: A Practical Guide to Improve Text Extraction - Medium
Shifting away from docx file manupulation
UP: Can it add tags inside docx file , I.e modify it?
Response: To directly answer your question: Gemini itself cannot "open" and modify a .docx file in the way you might open a file in Microsoft Word.
A .docx file is actually a compressed archive containing many internal XML files that handle styles, fonts, margins, and content. If you try to force an AI to "inject" XML tags into that complex internal structure, you will almost certainly corrupt the file, causing Word to report it as "unreadable" or "broken."
The "Developer" Solution
Since we are building a Python pipeline for your 100-article project, the correct way to "add tags" or modify the document structure is to keep the AI and the file manipulation separate.
- AI (Gemini): Used only to analyze the text, segment it, and generate the content or markup you want (the XML or tags).
- Python (File Manipulation): Use a specialized library like python-docx or docxtpl to handle the actual writing of the .docx file.
Recommended Workflow
If your goal is to add tags (like metadata or custom labels) to your articles within a Word document, use this approach:
- Extract: Use Python to read the text from your DOCX.
- Process: Send that text to Gemini and ask it to return the text with your tags added (e.g., <article_title>TITLE</article_title>).
- Rewrite: Use Python to create a new DOCX file, inserting the AI-tagged text into the document structure safely.
Why you should avoid "Direct Modification"
As the diagram shows, a .docx is not a single stream of text—it is a collection of parts. If you manually insert a tag in the wrong place, you break the relationship between the document.xml (content) and the styles.xml (formatting).
Summary for your Project
Since your end goal is to import these articles into MediaWiki, I strongly recommend skipping the DOCX modification step entirely.
- Don't try to tag the DOCX.
- Instead: Convert your OCR-DOCX files to Plain Text or Markdown once. Then, let the AI generate the final MediaWiki XML output directly from that text.
The MediaWiki XML format is designed specifically for importing articles, and it will handle the "tags" (like <title>, <text>, and [[links]]) much more reliably than trying to embed custom tags inside a Word document.
Would you like me to provide a Python script that takes your raw OCR text, uses Gemini to clean and structure it, and saves it directly as a .xml file ready for MediaWiki?
Sources
- How i can do Word(.docx) File Summarize using gemini/openAi's Apis - Google AI Developers Forum
- Python Series. Publishing code on PyPI: mod_docx XML… | by Practicing DatScy - Medium
UP: What about just a modification of the docx file
Response: If you want to modify a .docx file by adding tags (like metadata or custom labels) directly into it, you cannot do this by asking Gemini to "edit the file." Gemini generates text, not file-system operations.
To achieve this, you need to use a Python library called python-docx. This library acts as a bridge: it "unpacks" the DOCX structure, lets your script inject your tags, and then "re-packs" it into a valid Word document.
The Pipeline for Modifying a DOCX
If you want to insert tags around your titles and content, here is the technical logic:
- Read: python-docx opens the file.
- Process: You extract the text paragraph by paragraph and send it to Gemini.
- Tag: Gemini returns the text with your desired tags (e.g., <article>...</article>).
- Write: python-docx replaces the old paragraph with the new "tagged" version.
The Python Implementation
You will need to install the library first: pip install python-docx.
from docx import Document
import google.generativeai as genai
# Setup
doc = Document('input.docx')
genai.configure(api_key="YOUR_API_KEY")
model = genai.GenerativeModel('gemini-1.5-flash')
def add_tags_to_text(text):
prompt = f"Add <article> tags around the content and <title> tags around the article name. Text: {text}"
response = model.generate_content(prompt)
return response.text
# Iterate through paragraphs and modify
for para in doc.paragraphs:
if para.text.strip(): # If paragraph is not empty
para.text = add_tags_to_text(para.text)
doc.save('modified_output.docx')
Why this is safer than "Editing"
- Document Integrity: By using python-docx, you are modifying the underlying XML structure of the Word file correctly. If you try to do this with raw text editors, the file will corrupt.
- Batch Processing: You can run this on all 100 documents in minutes.
- Separation of Concerns: The AI provides the "intelligence" (deciding where the tags go), and the Python library provides the "structure" (the actual file modification).
Critical Warnings
- Style Preservation: The code above para.text = ... will remove existing formatting (bold/italic) because it treats the paragraph as plain text. If you need to keep the bold/italic formatting, you would need to use more complex code that iterates through para.runs.
- API Costs: This method sends every single paragraph to the API. If you have 100 long articles, this will be much more expensive than our previous "chunking" approach.
- OCR Artifacts: Since your source is OCR, the AI might hallucinate or break formatting in the .docx file if the OCR text is particularly messy.
Does this look like the level of control you were hoping for, or were you trying to insert tags as hidden "comments" or "metadata" instead of visible text?
UP: I mean if I try to add some arbitrary text into docx file. Is it possible to do with Google AI Studio
Response: No, it is not possible to use Google AI Studio to directly modify or inject text into an existing .docx file.
To clarify the distinction:
- Google AI Studio is a text-in, text-out interface. It processes information (like the raw text from your OCR files) and returns text/code/data as output. It does not have a file system or file-manipulation tools to open, "edit," save, or re-package binary file formats like .docx.
- The "Output" is just text: Even if you upload a .docx file to AI Studio for analysis, the model will output its response as raw text in the chat window. It cannot give you back the original .docx file with your edits saved inside it.
Why you cannot "just add arbitrary text" to a DOCX
If you were to try to use a text editor to force text into a .docx file, you would break the file. As shown in the visualization below, a .docx file is a complex "container" (a ZIP archive) that requires specific internal XML schema structures to be valid.
Your Path Forward
If your goal is to "tag" your articles by adding labels or metadata, you have two choices:
1. The "Clean" Pipeline (Highly Recommended)
- Extract: Read the text from your OCR-DOCX file using Python.
- Process: Send that text to the Gemini API (via your Python script) and ask it to output the text with your desired tags.
- Create: Use the Python python-docx library to create a brand-new .docx file and write the tagged text into it.
2. The "Annotation" Alternative
If you don't actually need the resulting file to be a "Microsoft Word" document, but simply need to store the tagged data:
- Save as XML/Markdown: Have your Python script save the AI's output directly into a .xml file. This is the format MediaWiki expects, and it is significantly easier to work with than a .docx file.
In summary: You can use Google AI Studio's intelligence to decide what text to add, but you must use Python code to perform the actual writing to the file.
Would you like the code snippet that shows how to take text from an existing file, have the AI "tag" it, and then write that result into a new .docx file?
markup vs app/api/ftext considerations, and getting rid of bloated html
UP: So, processing docx files would be cumbersome for Google AI Studio. How to make transition to plain text then without loosing formatting data?
Response: Transitioning from DOCX to a structured format like Markdown is the single most important step you can take to ensure your project succeeds. You are correct that treating DOCX as "just text" loses the structural formatting (the bolding and headings that define your article boundaries).
Here is the best way to convert your OCR-DOCX files while preserving that critical structural data.
1. The Strategy: Convert to Markdown
Markdown is a "plain text" format that keeps formatting (like bold, italics, and headings) as simple characters.
- Bolding: **text**
- Headings: ### Heading Title
- Lists: - Item
Because Markdown is just plain text, it will never "break" or become corrupted, but it gives the AI clear visual anchors to recognize your article boundaries.
2. The Recommended Tools
You do not need to do this manually. You can use Python to "translate" the DOCX styles into Markdown syntax automatically.
#knowledge: Using pandoc (The Gold Standard)
Pandoc is a command-line tool that is considered the "Swiss-army knife" of document conversion. It is incredibly good at converting DOCX styles directly into clean Markdown.
- How to use: pandoc input.docx -o output.md
- Why: It preserves the "Heading 1" or "Bold" tags from your Word file and converts them into Markdown # or ** syntax.
#knowledge; #critical; Using Python (mammoth)
If you want to keep this inside your Python project, use the mammoth library. Unlike other libraries, mammoth is specifically designed to convert DOCX to clean HTML or Markdown by mapping styles.
pip install mammoth
import mammoth
# This converts DOCX to Markdown/HTML, keeping your structure intact
with open("document.docx", "rb") as docx_file:
result = mammoth.convert_to_markdown(docx_file)
text = result.value # The text now has **bold** and # headings!
with open("output.md", "w", encoding="utf-8") as f:
f.write(text)
3. Why this solves your boundary problem
Once your OCR file is in Markdown, your "All Caps Bold" problem disappears:
- Before: You had to guess if text was bolded in the binary DOCX structure.
- After: Your titles will look like **TITLE OF ARTICLE**.
- The Result: You can now tell the AI: "Look for lines starting with ** and ending with **. Treat these as your <title> tags for the XML."
4. Checklist for a Smooth Transition
- Clean first: Before running the conversion, open the DOCX and ensure your OCR titles are consistently styled (e.g., if they are bold, make sure all of them are bold).
- Verify the Markdown: Open the .md file in any text editor. If the titles are wrapped in **, the AI will have zero trouble identifying the boundaries.
- Pipeline: Your flow should be: [DOCX] -> [Pandoc/Mammoth] -> [Markdown File] -> [Gemini API] -> [MediaWiki XML].
By converting to Markdown, you essentially "freeze" the formatting into a text-readable format that Gemini can process with 100% reliability, without the overhead of the complex DOCX format.
Does this "DOCX -> Markdown" pipeline sound manageable for your 100 files, or would you like help setting up a batch script to convert all of them at once?
UP: Does md format cater super and subscripts
Response: Standard Markdown does not have native, universal syntax for superscripts or subscripts. Because Markdown is designed to be lightweight and portable, it lacks these specific formatting features by default.
However, depending on the specific "flavor" of Markdown you are using (like Pandoc or specific platform extensions), you have three main ways to achieve this:
1. Using HTML Tags (Most Universal)
Because most Markdown processors are designed to render into HTML, you can use standard HTML tags directly in your Markdown file. This is the most reliable method that works across almost all platforms (including GitHub, Obsidian, and most web-based editors).
- Superscript: E = mc<sup>2</sup> renders as E = mc2
- Subscript: H<sub>2</sub>O renders as H2O
2. Using Markdown Extensions (Flavor-Specific)
Some Markdown processors support "Extended Syntax" through plugins. If your specific environment (like Pandoc or certain static site generators) has these enabled, you can use:
- Superscript: Surrounding text with ^ (e.g., 2^10^ becomes 210).
- Subscript: Surrounding text with ~ (e.g., H~2~O becomes H2O).
Note: Check your documentation to see if your tool supports these symbols, as they are not standard in "Vanilla" Markdown.
3. Using LaTeX/Math Blocks
If you are writing technical or scientific documentation, many Markdown processors (especially those used for academics or research) support LaTeX math notation. This is often triggered by wrapping the text in single or double dollar signs ($).
- Example: $H_2O$ or $E=mc^2$
Which should you use for your project?
Given that you are converting OCR-based DOCX files into MediaWiki XML, I strongly recommend using HTML tags (<sub> and <sup>).
Why?
- Compatibility: MediaWiki naturally understands HTML tags within its wikitext.
- Reliability: You do not have to worry about whether the specific Markdown flavor you choose supports ^ or ~.
- Readability: It makes it very clear to your processing script (and to the AI) exactly where the formatted text begins and ends.
If you are using the conversion script we discussed, you can simply instruct the AI in your System Prompt: "When encountering scientific notation or references, convert them to HTML <sub> and <sup> tags."
Sources
- add markdown superscript/subscript? · Issue #2171 · naturalcrit/homebrewery - GitHub
- Extended Syntax - Markdown Guide
- Markdown syntax - SuperMCP
UP: I tried to download a Google docs file as md. It doesn't support sup/sub scripts
Response: You are absolutely correct. Standard Markdown is a "lowest common denominator" format and does not natively support superscripts or subscripts. When you download as .md from Google Docs, that metadata is essentially discarded or ignored by the standard parser.
Since your ultimate destination is MediaWiki, you have a much better path than trying to force Markdown to carry that formatting.
The "Better Way" to Handle Sup/Sub
Instead of trying to "fix" Markdown, you should use HTML as your intermediate carrier. MediaWiki handles HTML tags perfectly, and Google Docs exports to HTML much more accurately than it exports to Markdown.
Your New, Reliable Pipeline:
- In Google Docs: Go to File > Download > Web Page (.html, .zip).
- Unzip the file: This gives you a clean .html file.
- Use the AI to Convert: Instead of pasting Markdown into the AI, paste the HTML content or simply upload the .html file to AI Studio.
- Prompt for Conversion: Use a prompt like this:"I am converting this HTML document to MediaWiki format. Please preserve all content, including <sub> and <sup> tags, as they are fully supported in MediaWiki. Ensure all other formatting (headers, bold, italics) is converted to native MediaWiki wikitext."
Why this works:
- No Information Loss: Unlike Markdown, which struggles with superscripts/subscripts, HTML is the language those tags were built for.
- Native MediaWiki Support: MediaWiki is built on top of a specialized parser that allows HTML tags like <sub>, <sup>, <b>, <i>, and others to coexist with wikitext.
- AI Precision: When you provide the AI with HTML, it can see the explicit tags (<sup>2</sup>), whereas in a broken Markdown export, that information might have been flattened into plain text (e.g., 2 instead of <sup>2</sup>).
Summary Comparison
| Format | Handles Sup/Sub natively? | MediaWiki Compatibility |
|---|---|---|
| Markdown (.md) | No (requires extensions) | Limited (requires specific parser settings) |
| HTML (.html) | Yes (via <sub>/<sup>) | Excellent (Native support) |
Recommendation: Stop fighting the Markdown conversion. Download as HTML, and use that HTML as the input for your Gemini processing. This will preserve your scientific and reference formatting without you having to manually re-apply it later.
Does that resolve the formatting bottleneck, or do you have other elements (like tables or footnotes) that were also getting mangled during the export?
UP: So, md can be like a filtered html. Only certain tags survive. It also can be an option, since when I downloaded html file it overloaded with html and css data which is really human unfriendly
Response: You are absolutely right. Markdown is essentially a "sanitized" version of HTML designed for readability. When you download a full .html file from Google Docs, you get "bloated" HTML—stuffed with <div> tags, internal CSS styles, class names, and JavaScript that you don't need.
However, you don't have to choose between "unreadable HTML" and "broken Markdown." Here is the professional way to get the best of both worlds:
1. The Strategy: "Clean" HTML
You can use a Python script to strip the bloat from your HTML file while keeping the meaningful tags (<sub>, <sup>, <b>, <i>, <h1>).
This keeps the data "human-readable" and preserves your formatting, but removes the CSS/Javascript garbage.
2. The Python Tool: BeautifulSoup
BeautifulSoup is the industry-standard tool for cleaning up messy HTML. You can use it to extract just the text and the specific tags you want, discarding the rest.
from bs4 import BeautifulSoup
# Load your bloated Google Docs HTML
with open("document.html", "r", encoding="utf-8") as f:
soup = BeautifulSoup(f, "html.parser")
# Remove all style and script tags (The "bloat")
for tag in soup(["style", "script", "head", "meta"]):
tag.decompose()
# Now you have a 'clean' HTML snippet
clean_html = str(soup.body)
3. The "Hybrid" Approach
If you want the absolute best results for your MediaWiki conversion, use this workflow:
- Download HTML from Google Docs.
- Run the Python Script above to strip the style, head, and script tags.
- Feed this "Clean HTML" to Gemini.
- Prompt Gemini: "Convert this HTML to MediaWiki wikitext. Keep all <sub>, <sup>, <b>, <i> tags as they are, and convert everything else to standard MediaWiki syntax."
Why this is superior:
- Preserves Structure: Unlike Markdown, which loses data, this method keeps your scientific notations (<sub>, <sup>) alive.
- Readable to the AI: By stripping the head and style tags, you reduce the token count (which saves you money) and prevent the AI from getting confused by irrelevant CSS classes.
- Zero Manual Effort: You don't have to "fix" anything manually. The Python script does the cleaning in milliseconds.
A Final Tip on MediaWiki
MediaWiki's own import tools are actually very happy with "Clean HTML." In many cases, you don't even need the AI to convert it to wikitext; you can paste the clean HTML directly into the MediaWiki editor, and it will render it perfectly.
Does this "strip the bloat" approach seem like a good balance for you, or do you find the HTML tags themselves still too "messy" to look at?
UP: So u insist to discard md and instead move to clean html?
Response: I am not saying you must discard Markdown, but rather that HTML is technically superior for your specific OCR-to-MediaWiki needs because of the sub/superscript requirement.
Think of it this way: Markdown is a convenience; HTML is the standard.
Here is the strategic trade-off so you can make the final call based on your comfort level:
The Comparison
| Feature | Markdown (.md) | Clean HTML |
|---|---|---|
| Superscript/Subscript | Not natively supported (requires hacks) | Native (<sup>, <sub>) |
| Human Readability | High (clean and simple) | Medium (has more tags) |
| AI Compatibility | Very High | Very High |
| MediaWiki Native | Needs transformation | Native-ready |
When to stick with Markdown
If your documents do not have heavy scientific, mathematical, or chemical notation, stick with Markdown. It is much easier to read, debug, and manage. You can simply use <sup> tags inside your Markdown files—most parsers will let you get away with "mixing" them.
When to move to Clean HTML
If your OCR source is heavily scientific (e.g., lots of chemical formulas, footnotes, or mathematical expressions), move to Clean HTML. It ensures that every bit of formatting from your OCR source survives the journey into MediaWiki without you having to manually fix broken characters.
My Professional Recommendation for Your Workflow
Don't choose "one or the other" based on a feeling. Choose based on the content of the individual file:
- For standard articles: Use the Markdown pipeline. It’s faster and cleaner.
- For scientific/technical articles: Use the Clean HTML approach.
- The "Universal" Script: You can write a single Python script that uses BeautifulSoup to clean the HTML, and then optionally converts it to Markdown. If the script detects that the HTML contains more than a certain number of <sub> or <sup> tags, it keeps the HTML format. Otherwise, it converts to Markdown.
To help you decide, does your OCR content include a high volume of scientific notation, or is it mostly standard prose with an occasional footnote?
UP: Sometimes html will contain intricate, nested and neighbouring span, b, i tags. Will beautiful soup be able to sanitize them automatically
Response: Yes, BeautifulSoup is the gold standard for this exact problem. It is specifically designed to handle "tag soup"—which is exactly what you get when you export messy, nested, and redundant tags from OCR-generated HTML.
You do not need to manually write complex rules for every <span>, <b>, or <i> combination. BeautifulSoup provides powerful, built-in ways to sanitize them automatically.
1. How BeautifulSoup handles the "Mess"
When you have nested tags like <b><i><span>text</span></i></b>, BeautifulSoup treats the HTML as a DOM tree. It allows you to "flatten" or "simplify" that tree without breaking the underlying text structure.
2. The "Sanitization" Script
You can use a simple function to strip out the "useless" tags (like <span> which often just carry CSS styles) while keeping the "meaningful" tags (like <b>, <i>, <sub>, <sup>).
from bs4 import BeautifulSoup
def sanitize_html(html_content):
soup = BeautifulSoup(html_content, "html.parser")
# 1. List of tags you want to KEEP
keep_tags = ['b', 'i', 'sub', 'sup', 'strong', 'em']
# 2. Iterate through all tags
for tag in soup.find_all(True): # find_all(True) gets every tag
if tag.name not in keep_tags:
# unwrap() removes the tag but keeps the inner content
tag.unwrap()
# 3. Optional: Flatten nested identical tags (e.g., <b><b>text</b></b> -> <b>text</b>)
# This keeps the output clean for the AI
return str(soup)
# Usage
clean_html = sanitize_html(bloated_html)
3. Why this solves your "Intricate/Nested" Problem
- tag.unwrap() is the key: This command is surgical. If you have <span><b>text</b></span>, calling unwrap() on the <span> leaves the <b> and the text behind. The tags vanish, but your content remains exactly where it was.
- Redundancy removal: Because BeautifulSoup understands the hierarchy, it doesn't get confused by "neighboring" tags. It processes the tree from the inside out, effectively collapsing the structure.
- AI-Ready: By the time this script finishes, your HTML will contain only the semantic tags that matter to MediaWiki. The AI will then receive a document that is structurally clean, making it much easier for it to generate the final XML output.
4. A Pro-Tip for your MediaWiki Pipeline
If your OCR-DOCX export produces a massive amount of <span> tags with styles like style="color:red;", just adding 'span' to the list of tags to unwrap() will delete the containers but save the text.
Does this look like it will handle the "tag soup" you are seeing, or are your files filled with complex tables and lists that might need more careful handling?
UP: What is the advantage of processing text documents on the source code level (md, html) compared to the processing over some API (OpenOffice, GoogleDocs, MSWord)? Which is better taking into account nowadays AI capabilities ?
Response: Processing documents at the source-code level (Markdown, HTML, Plain Text) is fundamentally superior to using proprietary API-based processing (like MS Word or Google Docs APIs) when the end goal is feeding data into an AI model.
The transition from binary/proprietary formats to structured text-based formats is not just a convenience—it is a performance optimization.
1. The Core Advantage: Semantic Clarity
When you feed an AI a .docx or a raw PDF through an API, the model receives a "flattened" or "bloated" representation. It has to spend its "intelligence" (and your tokens) filtering out noise.
- Source-Code Formats (Markdown/HTML): These act as semantic anchors. When you use # Title, **Bold**, or <sub>Subscript</sub>, you are giving the AI explicit structural instructions. The model doesn't have to "guess" if a line is a title based on its position on a page; it knows it is a title because of the tag.
- Proprietary API Formats: These rely on layout-based metadata. The AI must interpret complex internal XML or coordinate systems to understand that "Text at coordinate (x,y) with font-size 24" likely constitutes a heading. This increases the probability of "hallucinations" or logical errors.
2. Efficiency: The "Token" Economics
Modern AI APIs charge based on the number of tokens (roughly, chunks of characters) processed.
- API/Binary Bloat: Proprietary formats like .docx or raw HTML exports contain thousands of invisible tags, CSS classes, JavaScript, and metadata that the AI must read but that provide zero value for your content.
- Markdown/Cleaned HTML: These strip away the "container" and keep only the "content." You get more actual information per dollar because the model isn't wasting capacity on hidden formatting metadata.
3. Sanitization & Control
Processing at the source-code level allows for Deterministic Sanitization. As we discussed with BeautifulSoup, you can programmatically ensure that your documents are uniform before they reach the AI.
| Feature | Source-Code Level (MD/HTML) | Proprietary API (Word/Google) |
|---|---|---|
| Token Cost | Low (Clean data) | High (Markup/Metadata bloat) |
| Accuracy | High (Explicit structure) | Moderate (Layout-dependent) |
| Integration | Native/Direct | Requires middleware/API keys |
| Sanitization | Easy (Programmatic) | Hard (Requires proprietary tools) |
4. Which is better given today's AI capabilities?
Source-code level processing is the current industry standard for AI developers.
Today's most powerful models (like Gemini) excel at "Semantic Reconstruction." They prefer clean, structured inputs where the hierarchy of the information is already visible.
- The modern workflow: Use a Document Parser API to convert your raw/messy source (PDF, DOCX) into a clean, model-ready format like Markdown or Clean HTML before it ever touches your primary LLM.
Summary Recommendation: Do not make the AI "learn" the structure of your Word documents. Instead, use a programmatic "pre-processor" (like the Python scripts we discussed) to convert your documents into a clean, structural language (Markdown or Clean HTML) first. This transforms the AI’s task from "guessing document structure" to "analyzing content," which is where its power truly lies.
Are you ready to build the Python script that automates this "pre-processing" conversion, or would you like to explore how to handle specific edge cases like tables and lists first?
Sources
- Boosting AI Performance: The Power of LLM-Friendly Content in Markdown
- Your .docx Is Wasting 33% of Your AI Budget | by Marc Bara | Medium
- Choose the Right File Format: A Practical Guide for Fast, Accurate Data Extraction - Medium
- What formats can you feed web data to AI? | Firecrawl Glossary
- Best Document Parsing APIs to Try in 2026 - Firecrawl
UP: What about non-propriotory OpenOffice API based on UNO objects written in StarBasic?
Response: Using the OpenOffice/LibreOffice UNO (Universal Network Objects) API via StarBasic is a "classic" approach, but it is highly inefficient for modern AI pipelines and carries several technical risks.
While it technically allows you to manipulate documents at an object level, it is the opposite of the "clean pipeline" approach we just established.
1. Why avoid the UNO/StarBasic approach?
- Heavyweight Execution: To process a document via UNO, you essentially have to spin up a "headless" instance of the entire OpenOffice/LibreOffice suite in the background. This is resource-intensive and slow compared to using a lightweight library like BeautifulSoup or python-docx.
- Complexity of "Objects": In the UNO API, you aren't dealing with text; you are dealing with XTextCursor, XTextRange, and complex property sets. This is "Stateful" programming—you have to manage the position of the cursor and the state of the document object model manually.
- Non-Parallelizable: You generally cannot easily run multiple instances of the OpenOffice process to speed up your 100-article project without running into massive memory usage issues or file lock conflicts.
- StarBasic Limitations: StarBasic is an older, proprietary language. It lacks the modern ecosystem of data processing libraries (like pandas, re, BeautifulSoup, or google-genai) that you would need to easily bridge your data to the Gemini API.
2. The Difference in Philosophy
- The "Legacy" way (UNO/StarBasic): You use a word processor engine to "open" the file, move a cursor through it, and extract text object-by-object. You are working at the application level.
- The "Modern" way (Markdown/HTML/Python): You treat the document as a data structure. You don't need a word processor to exist; you just need to parse the underlying XML or text. You are working at the data level.
3. When would you use UNO?
You should only consider the UNO API if:
- You have complex macro-based automation inside the documents that must be triggered to resolve correctly.
- The documents use extremely specific, proprietary ODF (OpenDocument Format) features that standard XML parsers cannot interpret.
- You are already working inside the OpenOffice environment as a primary tool.
Recommendation: Stick to Python + Clean Text
For your project, do not go down the StarBasic/UNO route. It will add layers of "technical debt" to your project.
- Maintainability: If you use Python with BeautifulSoup or mammoth, your code is portable and easy to troubleshoot.
- Integration: If you use the UNO/StarBasic route, you then have to figure out how to bridge StarBasic to the Gemini Python API—which is an unnecessary headache.
If you are looking to automate the transition of your 100 files, I can help you write a simple Python script using mammoth or BeautifulSoup that is much faster, cleaner, and easier to connect to the Gemini API than any StarBasic macro ever could be. Would you like to see a comparison of how many lines of code it takes to extract text from a DOCX using the UNO API vs. a modern Python library?
UP: I heard that UNO API is manageable by Python as well. Does it change the situation
Response: Yes, you are technically correct: The LibreOffice/OpenOffice UNO API can be accessed via Python (using the uno module). However, even when used with Python, it does not change the fundamental drawbacks for your specific AI-driven MediaWiki project.
Here is the breakdown of why "UNO via Python" is still not the right tool for your current workflow.
1. The "Interface" Problem (Not the Language)
The problem isn't the language (Python vs. StarBasic); the problem is the target interface.
- The UNO API is designed to control a desktop application's UI, layouts, styles, and "view" states. It is built to mimic a human user interacting with the software.
- Your Project is a data-processing pipeline. You don't need a UI, you don't need to "open" a window, and you don't need to manage document "views." You just need the text and the metadata.
2. Performance Comparison
Using UNO via Python forces your script to act like a remote control for a heavy word processor. Compare this to a "Direct Access" method:
| Feature | UNO via Python | Modern Parsing (BeautifulSoup/mammoth) |
|---|---|---|
| Footprint | Loads full office suite (500MB+ RAM) | Loads only library (10MB RAM) |
| Speed | Slow (requires document "rendering") | Near-instant (stream parsing) |
| Complexity | High (complex object nesting) | Very Low (simple tags) |
| Stability | Prone to crashes (UI-related) | Very Stable |
3. "AI Pipeline" Conflict
When you use the UNO API, you are extracting "Document Objects." You then have to write a second layer of code to translate those objects into a format the AI can understand.
If you use Markdown or Clean HTML (as we discussed), the document is already in the language that modern AI models understand natively. By using the UNO API, you are adding an unnecessary translation layer that increases the likelihood of bugs and data loss.
The Only Time UNO via Python is the "Right" Choice
There is only one scenario where I would tell you to use the UNO API: If your documents are so badly formatted or rely on obscure ODF features that standard parsers fail completely. If you try mammoth or BeautifulSoup and find that the output is garbled—because the OCR-DOCX files have complex "frame-based" positioning or custom ODF styles that don't export to clean text—then and only then would the UNO API be a necessary "last resort."
Game changing recomendation: mammoth(docx)
Recommendation
For 100 articles, don't start with the most complex tool. 1. Start simple: Use mammoth (for DOCX) or BeautifulSoup (for HTML). It will take you 10 minutes to write the script. 2. Test: See if the output is high-quality. 3. Evaluate: If (and only if) the output is broken, look into the UNO API.
Would you like me to write a short "proof-of-concept" script using mammoth so you can see how quickly you can convert your files without touching the UNO API?
UP: Ten years ago when there was no AI tools I had tried both macro based parsing and text source code parsing and preferred first option, since the html obtained from saving the text document with different apps (MSWord, OpenOffice) contained very messy tags.
Response: Your preference for macro-based parsing ten years ago was the correct, logical engineering decision at that time. A decade ago, "Save as HTML" was essentially a "Save as Garbage" button—it produced documents bloated with proprietary <mso-application> tags, v:shape objects, and fixed-position absolute CSS that made text extraction a nightmare.
However, the landscape has shifted drastically, and your experience is actually the perfect setup for why we recommend the current "Modern Pipeline."
The "Clean" Revolution
The "messy tags" problem you experienced hasn't just been solved—it has been fundamentally bypassed. We no longer rely on the "Save as HTML" features of Word or OpenOffice. We now use conversion libraries that act as professional translators.
1. The "Modern Pipeline" vs. "Legacy Macros"
- Legacy (Your old method): You used Macros/UNO to "read" the document because the Export filter was terrible. You were essentially doing the work of an intelligent parser manually, which is why it was the most reliable option at the time.
- Modern (Our recommended method): We don't "Export." We "Transform." Libraries like mammoth (for DOCX) or pandoc act as a middleman that reads the raw XML structure of a file and emits clean, semantic Markdown or HTML.
2. Why the AI Factor changes everything
When you were doing this 10 years ago, you had to write a macro to understand what was a title and what was a paragraph. You were building the logic yourself in StarBasic/VBA.
Today, you are outsourcing the "Understanding" to the AI.
- Then: You had to program logic: If FontStyle = Bold and FontSize > 14 Then Title. If the OCR was slightly off, your macro failed.
- Now: You just feed the text to Gemini. The model "understands" the context. If the OCR is slightly messy, the AI says: "Oh, this looks like a title, I will treat it as a title," even if the underlying code is imperfect.
3. The "Hybrid" Strategy
Since you already have the instinct for macro-based parsing, I have a recommendation that honors your experience while adopting modern speed:
If you are worried about the messy tags of a "Save as HTML" export, don't export it yourself. Use a Python script to do the "Save as" logic for you using mammoth.
import mammoth
# This bypasses the bloated 'Save as HTML' from Word entirely
# It pulls directly from the .docx file's XML 'content' parts.
with open("article.docx", "rb") as docx_file:
result = mammoth.convert_to_markdown(docx_file)
print(result.value) # Returns clean, readable text
This method doesn't use the OpenOffice "Save" filters. It reads the raw file structure and discards all the CSS/VML/Javascript bloat that caused your previous frustration.
My suggestion to "Bridge" your experience:
You are right to be skeptical of messy HTML exports. But since you have 100 articles, please try a quick test of the modern Python mammoth library on just ONE of your documents. Compare the output of that to the macro-based results you used to get. If the mammoth output is clean (which it likely will be, as it specifically ignores the visual "bloat" you hated), you will save yourself weeks of manual programming.
Does it sound like a fair test to try one file through a modern library to see if it solves the "messy tags" problem you faced a decade ago?
UP: are u sure beautiful soup or mammoth can handle html like this:
<html><head><meta content="text/html; charset=UTF-8" http-equiv="content-type"><style type="text/css">ol{margin:0;padding:0}table td,table th{padding:0}.c5{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:20pt;font-family:"Arial";font-style:normal}.c4{color:#000000;font-weight:400;text-decoration:none;vertical-align:sub;font-size:11pt;font-family:"Arial";font-style:italic}.c6{color:#000000;font-weight:700;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.c1{color:#000000;font-weight:400;text-decoration:none;vertical-align:super;font-size:11pt;font-family:"Arial";font-style:italic}.c0{color:#000000;font-weight:700;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:italic}.c7{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:italic}.c8{padding-top:20pt;padding-bottom:6pt;line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.c2{padding-top:0pt;padding-bottom:0pt;line-height:1.15;orphans:2;widows:2;text-align:left}.c3{background-color:#ffffff;max-width:468pt;padding:72pt 72pt 72pt 72pt}.c9{height:11pt}.title{padding-top:0pt;color:#000000;font-size:26pt;padding-bottom:3pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.subtitle{padding-top:0pt;color:#666666;font-size:15pt;padding-bottom:16pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}li{color:#000000;font-size:11pt;font-family:"Arial"}p{margin:0;color:#000000;font-size:11pt;font-family:"Arial"}h1{padding-top:20pt;color:#000000;font-size:20pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h2{padding-top:18pt;color:#000000;font-size:16pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h3{padding-top:16pt;color:#434343;font-size:14pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h4{padding-top:14pt;color:#666666;font-size:12pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h5{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h6{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}</style></head><body class="c3 doc-content"><h1 class="c8" id="h.bsuvd03lmykq"><span class="c5">Heading 1</span></h1><p class="c2 c9"><span class="c6"></span></p><p class="c2"><span class="c0">Italic and bold</span></p><p class="c2"><span class="c7">Just italic</span></p><p class="c2"><span class="c1">Superscript</span></p><p class="c2"><span class="c4">subscript</span></p></body></html>
Response: Your skepticism is completely justified—that looks exactly like the "bloated" export I warned you about. You have CSS classes (like c1, c4, c5) that map to visual properties, and the content is essentially "trapped" inside those classes.
The short answer: No, BeautifulSoup or mammoth will not automatically understand that .c1 means "Superscript" without a little bit of help from you.
If you run a basic script, it will just give you the text, and you'll lose the formatting. However, you can solve this easily using a "Mapping Function."
1. The Strategy: Class-to-Tag Mapping
Since the HTML is consistent (the CSS classes are defined in the <style> block), you can write a short Python function that "translates" those classes into real HTML tags before the AI ever sees them.
Here is how you handle that exact snippet of code:
from bs4 import BeautifulSoup
def clean_and_map_classes(html_content):
soup = BeautifulSoup(html_content, "html.parser")
# Define your mapping based on the <style> block provided
# c1 is vertical-align:super -> <sup>
# c4 is vertical-align:sub -> <sub>
# c0 is font-weight:700 (bold) + italic -> <b><i>
mapping = {
"c1": "sup",
"c4": "sub",
"c0": "b_i", # Custom placeholder for bold+italic
"c6": "b" # bold
}
for class_name, tag_name in mapping.items():
for span in soup.find_all("span", class_=class_name):
# Wrap the content in the target tag
if tag_name == "b_i":
span.wrap(soup.new_tag("i"))
span.wrap(soup.new_tag("b"))
else:
span.wrap(soup.new_tag(tag_name))
# Remove the span wrapper
span.unwrap()
# Remove remaining garbage (styles, metadata, empty divs)
for tag in soup(["style", "head", "meta", "body"]):
# Note: We only want to unwrap the head/body to keep the text
if tag.name != 'body': tag.decompose()
else: tag.unwrap()
return str(soup)
2. Why this is the "Professional" way
- It’s Deterministic: You aren't guessing. You are telling the computer: "Whenever you see c1, that is a superscript." Because your Google Docs/Word exports consistently use the same style sheets, this mapping will work for all 100 files perfectly.
- It's AI-Ready: Once your script runs, that <span> soup is transformed into <sup>Superscript</sup>. Now, when you feed it to Gemini, it doesn't have to "interpret" the CSS—the structure is already explicitly defined.
- No Manual Macros: You are still using Python, not StarBasic/UNO, so you keep all the speed and library advantages we discussed.
3. The "Hybrid" AI-Assisted Shortcut
If you have 100 files and don't want to map every c1, c2, c3 by hand, you can use the AI to write the mapper for you.
Try this:
- Paste your <style> block (the CSS) into Gemini.
- Ask: "Write a Python dictionary mapping these CSS classes to their semantic HTML5 equivalent (e.g., c1: sup, c4: sub)."
- The AI will generate the mapping dictionary in seconds.
- Paste that dictionary into your Python script.
This gives you the accuracy of a macro-based parser but with the speed and flexibility of modern Python. Does this "Mapping" approach feel like it would solve the messiness while keeping the logic strictly under your control?
Application of Mammoth lib
UP: will mammoth produce me a sanitized html of the docx file keeping formatting intact?
Response: Yes, Mammoth is exactly designed for this purpose, but with one critical distinction: it aims for semantic integrity, not visual mimicry.
Because Mammoth is built to provide "clean" HTML for the web, it intentionally discards "presentational" formatting (like specific fonts, exact text colors, or margins) because those things usually break websites. However, it does preserve the structural and semantic formatting you need for an AI pipeline.
What Mammoth Will Keep
Mammoth is excellent at extracting the "meaning" of your document. It will reliably convert:
- Superscripts and Subscripts: These are natively supported (<sup>, <sub>).
- Headings: Converts Word's "Heading 1", "Heading 2", etc., to <h1>, <h2>, etc.
- Bold and Italics: Converts them to <strong>/<b> and <em>/<i>.
- Lists: Converts bulleted and numbered lists into valid HTML <ul>/<ol> structures.
- Tables: Converts DOCX table objects into standard HTML <table> elements.
- Links: Preserves your hyperlinks.
What Mammoth Will Discard (The "Sanitization")
This is where Mammoth saves you from the "messy tags" you hated in the past. It will strip out:
- Inline CSS: No more style="font-family: Arial; color: #000000;" cluttering your content.
- Proprietary Microsoft XML namespaces: It ignores the hidden mso-* tags that plague MS Word exports.
- Fixed-positioning/Layout junk: It ignores text boxes, absolute positioning, and floating frames, instead rendering their content as standard paragraphs in the document flow.
Why this is better than your old "Macro" approach
When you were doing this 10 years ago, you were essentially fighting against the "Save as Web Page" feature, which tries to make the HTML look exactly like the Word doc—resulting in the "spaghetti code" you experienced.
Mammoth takes the opposite approach: It treats the DOCX file as a data source. It looks at the internal XML of the DOCX, extracts the semantic intent of the text, and writes fresh, clean HTML from scratch.
How to use it for your 100-article project
Since you are concerned about consistency, you can control exactly how it behaves. If the default mapping isn't perfect, you can provide a "Style Map."
Example: If you have custom styles in your Word doc, you can define them:
import mammoth
style_map = """
p[style-name='Section Title'] => h1:fresh
p[style-name='Warning'] => div.warning > p
"""
with open("article.docx", "rb") as docx_file:
result = mammoth.convert_to_html(docx_file, style_map=style_map)
clean_html = result.value
My Verdict: For your MediaWiki migration, Mammoth is likely the most efficient tool available. It will produce the exact "clean" HTML you need to feed into Gemini without the CSS bloat that would have required a complex sanitization script.
Would you like me to show you how to wrap this into a simple loop to process all 100 of your files at once?
UP: am i required to specify style_map or mammoth can save html without it?
Response: No, you are not required to specify a style_map.
Mammoth is designed to work "out of the box." If you don't provide a style_map, Mammoth uses its internal default mapping, which handles the most common Word styles automatically.
What Mammoth does by default:
Without any custom configuration, Mammoth will automatically convert standard Word formatting into clean, semantic HTML:
- Paragraphs: Standard paragraphs become <p> tags.
- Headings: "Heading 1" becomes <h1>, "Heading 2" becomes <h2>, etc.
- Lists: Bulleted and numbered lists become <ul> and <ol> tags.
- Emphasis: Bold text becomes <strong> (or <b>), and italics become <em> (or <i>).
- Hyperlinks: URLs are preserved as <a> tags.
- Tables: Tables are converted into valid HTML <table> structures.
- Superscript/Subscript: These are generally preserved as <sup> and <sub> tags.
When should you use style_map?
You only need to specify a style_map if your document uses custom styles that don't match the standard Word defaults (e.g., if you have a specific style called "Important Notice" that you want to appear as a <div class="warning"> in your HTML).
Summary for your project
Since your goal is to feed this into an AI for MediaWiki conversion, the default output is actually perfect for your needs because it is already "sanitized" and semantic.
- Try the default first: Simply run the basic mammoth.convert_to_html(docx_file) on one of your files.
- Check the result: If you find that it correctly captures your bold, italic, and sub/superscript text, you are done! No extra code is needed.
- Add mapping only if necessary: If you find a specific, non-standard style in your OCR-DOCX that Mammoth isn't handling correctly (like a custom "Highlighted" style that is being ignored), that is the moment to write a specific style_map rule for it.
You can save yourself a lot of work by skipping the custom mapping unless the default output proves insufficient for a specific article.
UP: what is this: p[style-name='Warning'] => div.warning > p
Response: That line of code is a transformation rule for the mammoth library. It tells the parser how to take a specific bit of "Word-land" formatting and turn it into "Web-land" code.
Here is the breakdown of what each part means:
- p[style-name='Warning'] (The "Find" side):
- p refers to a paragraph.
- [style-name='Warning'] is the filter. It tells Mammoth to look for any paragraph in your Word document that has been tagged with the specific style name "Warning".
- => (The "Action"):
- This is the operator that tells Mammoth, "When you find the item on the left, replace it with the structure on the right."
- div.warning > p (The "Replace" side):
- div.warning creates an HTML <div> container with the class name "warning".
- > p tells Mammoth to place the content of the original paragraph inside that <div> as a <p> tag.
Why is this powerful?
Instead of just getting a generic <p> tag, this rule allows you to map your document's internal logic to meaningful HTML.
If you have a block of text in Word that you always mark as a "Warning" style, your output will not just look like a paragraph; it will become a semantic HTML block:
<div class="warning">
<p>This is your warning text.</p>
</div>
Why you would use this for your 100 articles:
If your OCR source uses consistent, non-standard styles for things like "Article Definitions", "Citations", or "Metadata", you can use these rules to automatically turn those into clean, CSS-friendly HTML classes.
This makes the final MediaWiki import much easier because you can use CSS on the MediaWiki site to style those "warnings" or "definitions" globally without needing to edit 100 articles individually.
Would you like to try defining a rule for one of your specific document styles to see if it makes your final output cleaner?
UP: how to write clean_html into a file
Response: Writing your cleaned HTML to a file is straightforward using Python's built-in file handling. Since you are using BeautifulSoup to process your documents, you want to make sure you save the file with UTF-8 encoding to preserve all special characters, sub/superscripts, and formatting.
Here is the standard, most reliable way to save your processed content:
# Assuming 'clean_html' is the string variable containing your sanitized HTML
file_path = "output_article.html"
with open(file_path, "w", encoding="utf-8") as file:
file.write(clean_html)
print(f"Successfully saved clean HTML to {file_path}")
Key Considerations for your Pipeline:
- Always use encoding="utf-8": This is non-negotiable. Without it, your scientific symbols or specific characters might get corrupted when saving to disk, especially if you are on Windows.
- Structuring your Output: Since you have 100 articles, you likely want to keep your project organized. You can modify the file_path dynamically using the original filename: import os input_filename = "article_01.docx" # This creates a new filename like 'article_01_clean.html' output_filename = os.path.splitext(input_filename)[0] + "_clean.html" with open(output_filename, "w", encoding="utf-8") as f: f.write(clean_html)
- Pretty Printing (Optional): If you find yourself needing to manually debug the HTML and want it to be "human-readable" (with proper indentation), BeautifulSoup has a built-in feature for this: # Instead of str(soup), use: pretty_html = soup.prettify() with open(output_filename, "w", encoding="utf-8") as f: f.write(pretty_html) Note: Use prettify() only for human inspection. It adds extra whitespace that you don't need for the AI or the MediaWiki import.
This approach is clean, fast, and creates the exact intermediate file format you need to feed into Gemini or upload to your MediaWiki instance.
Would you like me to put this into a complete loop so you can process all 100 files in one go?
UP: combine this code with previous mammoth.convert_to_html code
Response: To create an efficient pipeline for your 100 articles, we will combine the mammoth conversion (to get clean semantic HTML) with the Python file-writing logic.
This script will loop through all .docx files in a folder, convert them to clean HTML, and save each one with a _clean.html suffix.
The Combined Pipeline Script
import mammoth
import os
# 1. Define your folder containing the .docx files
input_folder = "my_articles"
output_folder = "cleaned_articles"
# Create output folder if it doesn't exist
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# 2. Loop through all files in the directory
for filename in os.listdir(input_folder):
if filename.endswith(".docx"):
file_path = os.path.join(input_folder, filename)
# 3. Convert DOCX to clean HTML using mammoth
with open(file_path, "rb") as docx_file:
result = mammoth.convert_to_html(docx_file)
clean_html = result.value # The cleaned, semantic HTML
# 4. Define the output filename
output_filename = os.path.splitext(filename)[0] + "_clean.html"
output_path = os.path.join(output_folder, output_filename)
# 5. Write the result to a file with UTF-8 encoding
with open(output_path, "w", encoding="utf-8") as f:
f.write(clean_html)
print(f"Processed: {filename} -> {output_filename}")
print("Done! All files have been converted.")
Why this structure works perfectly for you:
- Encapsulation: By keeping the processing inside a for loop, you don't have to worry about manual naming or file handling. The script does it all.
- Memory Efficiency: Because mammoth processes one file at a time, this script will run smoothly even if you had 1,000 documents instead of 100.
- Safety: The os.path functions ensure that your system paths remain correct, preventing "File Not Found" errors.
- UTF-8 Assurance: By explicitly setting encoding="utf-8", you guarantee that your subscript/superscript tags and any special scientific characters remain intact during the save process.
Pro-Tip for your MediaWiki migration:
If you find that your files have consistent custom styles (like specific headers for document sections) that you want to map to custom HTML classes, you can simply add the style_map argument inside the mammoth.convert_to_html() function call within this loop.
Does this script look like it fits into your workflow, or do you need to add any specific logic to handle sub-folders or special naming conventions for your 100 articles?
UP: how to get filename from file_path
Response: To get the filename from a file path in Python, the standard and most reliable approach is to use the os.path module or the modern pathlib module.
1. The Modern Approach (pathlib) - Recommended
pathlib is the current standard for path manipulation in Python (available since 3.4). It treats paths as objects rather than just strings.
from pathlib import Path
file_path = "/path/to/your/article_01.docx"
# Get the filename (e.g., 'article_01.docx')
filename = Path(file_path).name
# Get just the filename without the extension (e.g., 'article_01')
filename_no_ext = Path(file_path).stem
print(filename)
print(filename_no_ext)
2. The Traditional Approach (os.path)
If you are working with older codebases or prefer the classic approach, os.path is the standard library module used for years.
import os
file_path = "/path/to/your/article_01.docx"
# Get the filename (e.g., 'article_01.docx')
filename = os.path.basename(file_path)
# Get the filename without the extension
filename_no_ext = os.path.splitext(filename)[0]
print(filename)
print(filename_no_ext)
Which one should you use?
- Use pathlib if you are writing new code. It is more readable and handles cross-platform path differences (like / vs \) much more gracefully.
- Use os.path if you are integrating into a very old script or need to maintain strict compatibility with Python versions older than 3.4 (though this is increasingly rare).
For your loop of 100 articles, the pathlib approach is cleaner and less prone to errors.
Does this make it easy to name your output files correctly in the script we just built?
UP: what is this: os.path.splitext(filename)[0]
Response: This is a very common Python idiom used to strip the file extension from a filename.
Here is the breakdown of how it works:
1. The Breakdown
- os.path.splitext(filename): This function splits a path into a pair (a tuple) containing two parts: the "root" (the name) and the "extension".
- Example: If filename is "article_01.docx", this function returns: ('article_01', '.docx').
- [0]: This tells Python to grab only the first element of that pair (the index 0).
- Result: It gives you just "article_01".
2. Why is this useful?
When processing files in bulk, you almost always need to change the extension (e.g., from .docx to .html) without losing the original filename.
Without this method: You would have to manually search for the position of the dot (.) and slice the string, which is prone to errors if the filename contains multiple dots (like my.article.v1.docx).
With this method: It handles the logic for you reliably:
import os
filename = "my.document.docx"
root = os.path.splitext(filename)[0]
print(root)
# Output: 'my.document'
3. A Practical Example in your Script
In your script, you are converting .docx to .html. Using this allows you to create the new name dynamically:
original_name = "data_analysis.docx"
new_name = os.path.splitext(original_name)[0] + "_clean.html"
# new_name becomes "data_analysis_clean.html"
It is a clean, robust, and standard way to manipulate filenames in Python. Does this help you see how your script will handle the naming of all 100 files automatically?
UP: how to get folder name containing the file from the file_path
Response: To get the name of the folder that contains a specific file, you are essentially looking for the "parent directory" name. Depending on whether you want the full path to the folder or just the last folder name, here are the two common methods.
1. The Modern Approach (pathlib) — Recommended
This is the cleanest and most readable way to handle paths in modern Python.
from pathlib import Path
file_path = "/Users/yourname/projects/my_articles/article_01.docx"
# Get the path to the folder containing the file
folder_path = Path(file_path).parent
# Get the name of just that folder (e.g., 'my_articles')
folder_name = folder_path.name
print(f"Full path: {folder_path}")
print(f"Folder name: {folder_name}")
2. The Traditional Approach (os.path)
If you prefer the classic approach, use os.path.dirname to get the path and os.path.basename to isolate the name of that directory.
import os
file_path = "/Users/yourname/projects/my_articles/article_01.docx"
# Get the directory path (e.g., '/Users/yourname/projects/my_articles')
dir_path = os.path.dirname(file_path)
# Get the last part of that directory path (e.g., 'my_articles')
folder_name = os.path.basename(dir_path)
print(f"Full path: {dir_path}")
print(f"Folder name: {folder_name}")
Why this is useful for your 100-file project
If your 100 articles are organized in sub-folders (e.g., biology/article1.docx, physics/article2.docx), you can use this logic to maintain that folder structure in your output.
For example, you could save your processed files into a corresponding output folder by doing this:
# Assuming 'file_path' is your input
folder_name = Path(file_path).parent.name
# Create a matching output folder
output_dir = f"output_{folder_name}"
os.makedirs(output_dir, exist_ok=True)
# Now save the file inside that specific sub-folder
output_path = os.path.join(output_dir, output_filename)
By extracting the parent folder name, you can ensure that your processed files are categorized exactly the same way as your original OCR files.
Does this help you organize your output files, or were you planning to dump all 100 files into one single folder?
UP: What are the key functions of a parser?
Response: A parser is a fundamental component in computer science that bridges the gap between raw, unstructured input and a structured representation that a computer or AI can process. In the context of your document-to-MediaWiki pipeline, the parser's role is to convert "messy" data into a logical, hierarchical format.
Here are the key functions of a parser:
1. Lexical Analysis (Scanning)
Before a parser can understand structure, it must identify the "atoms" of the document. The scanner reads the input character-by-character and groups them into meaningful units called tokens.
- Example: In your HTML, it identifies <h1>, <span>, and c5 as individual tokens rather than just a long string of characters.
2. Syntactic Analysis (Parsing)
This is the core function where the parser checks the tokens against a set of rules (grammar). It ensures the structure of the document is valid according to the expected format (like HTML5 or Markdown).
- Structural Mapping: It builds a Parse Tree (or Abstract Syntax Tree). It recognizes that a <span> inside a <h1> is a child of that heading, allowing it to understand the relationship between different elements.
3. Semantic Analysis
This is where the parser moves from "syntax" (is the code valid?) to "meaning" (what does this structure represent?).
- Interpretation: For your project, this is where the parser understands that a <sup> tag means "superscript" and should be rendered as such in MediaWiki, rather than just treating it as a literal string of text.
4. Transformation (Normalization)
Modern parsers (like BeautifulSoup or mammoth) perform the critical function of Normalizing the input. They take inconsistent, non-standardized input and force it into a clean, uniform output format.
- Cleaning: It strips away proprietary junk, unnecessary metadata, and redundant tags that provide no value to the final output.
Summary of Parsing Benefits
By performing these functions, a parser provides three major advantages for your migration project:
- Error Handling: It identifies where a document might be "malformed" (e.g., an unclosed tag) and attempts to repair it or alerts you, rather than letting the error propagate to the MediaWiki or the AI.
- Abstraction: It allows you to write code that cares about what is in the document (the content) without worrying about how the text is positioned or stored in the binary file.
- Efficiency: It converts heavy, bloated proprietary files into compact, semantic representations, which reduces token usage and processing time for the LLM.
Is there a specific part of the parsing workflow that you are concerned will be difficult for your specific 100-article collection, such as complex nested tables or specific footnote structures?
UP: I want my python sanitizer code produce output into the 'output' folder, which is located one level higher of the code file. How to get the path where my code file is located and then its parent folder?
Response: To get the directory where your Python script is located, and then navigate one level higher to your parent folder, you should use the modern pathlib module.
Here is how you do it cleanly and safely:
from pathlib import Path
# 1. Get the folder where the current Python script is located
script_dir = Path(__file__).resolve().parent
# 2. Get the parent folder (one level higher)
parent_dir = script_dir.parent
# 3. Define your 'output' folder inside that parent directory
output_folder = parent_dir / "output"
# Create the output folder if it doesn't already exist
output_folder.mkdir(parents=True, exist_ok=True)
print(f"Script location: {script_dir}")
print(f"Output folder location: {output_folder}")
Breakdown of the Magic:
- __file__: A built-in Python variable that holds the path of the script currently being executed.
- .resolve(): Ensures you get the absolute, canonical path (resolving any relative paths or symlinks).
- .parent: Moves you up one directory level. Doing it once (script_dir.parent) takes you to the parent folder. Doing it twice (script_dir.parent.parent) would take you up two levels.
- / operator: pathlib lets you use the forward slash operator to cleanly join paths together, replacing os.path.join().
UP: os lib cannot do it?
Response: Yes, the traditional os library can definitely do it! While pathlib is the modern standard, os and os.path have been doing this for decades.
Here is how you achieve the exact same thing using the os module:
import os
# 1. Get the folder where the current Python script is located
script_dir = os.path.dirname(os.path.abspath(__file__))
# 2. Get the parent folder (one level higher) using os.path.dirname again
parent_dir = os.path.dirname(script_dir)
# 3. Define your 'output' folder inside that parent directory
output_folder = os.path.join(parent_dir, "output")
# Create the output folder if it doesn't already exist
if not os.path.exists(output_folder):
os.makedirs(output_folder)
print(f"Script location: {script_dir}")
print(f"Output folder location: {output_folder}")
How os handles it:
- __file__: Just like with pathlib, this points to your current script file.
- os.path.abspath(__file__): Gets the full absolute path of the script (similar to .resolve()).
- os.path.dirname(...): Strips away the filename to leave you with the directory. If you wrap it twice (os.path.dirname(os.path.dirname(...))), it moves you up two levels, but doing it once takes you up one level to the parent directory.
- os.path.join(...): Safely glues paths together, handling slashes properly regardless of whether you are on Windows, Mac, or Linux.
UP: import mammoth line doesn't see the lib anymore... Hence the pip install mammoth Requirement already satisfied: mammoth in /opt/homebrew/lib/python3.11/site-packages (1.12.0)Requirement already satisfied: cobble<0.2,>=0.1.3 in /opt/homebrew/lib/python3.11/site-packages (from mammoth) (0.1.4)
Response: This is a classic Python environment mismatch. When you run pip install, it is installing the package into your Homebrew Python environment (/opt/homebrew/lib/python3.11/site-packages), but when you run your script, your terminal or IDE is executing it using a different Python interpreter.
Here is how to fix this quickly:
1. Verify which Python is running your script
Add these lines to the top of your script and run it again to see where Python is looking for libraries:
import sys
print(sys.executable)
print(sys.path)
If the printed executable path is not /opt/homebrew/bin/python3 (or similar), that explains why it can't find mammoth.
2. Force install to the active Python interpreter
Instead of running a generic pip install, explicitly target the Python interpreter you want to use by invoking it directly:
python3 -m pip install mammoth
(On macOS, if you are using Homebrew specifically, you might need to use python3.11 -m pip install mammoth).
3. Check your IDE (VS Code, PyCharm, etc.)
If you are using an IDE:
- VS Code: Look at the bottom right corner of the window. It shows the active Python environment version. Click it and select the Homebrew Python environment (Python 3.11.x) that matches where pip installed mammoth.
- Terminal/Command Line: Make sure you aren't accidentally inside a virtual environment (like a .venv or venv folder) where mammoth hasn't been installed yet. If you want to use a virtual environment, activate it first (source venv/bin/activate) and then run pip install mammoth.
UP: p[style-name='Image Caption'] => h1:fresh what does colon mean here
Response: In Mammoth’s style map syntax, the colon (:) acts as a modifier (similar to a CSS pseudo-class) that tweaks how the HTML element is generated.
Specifically, :fresh tells Mammoth to force a brand-new HTML element rather than merging or reusing adjacent ones.
Why does :fresh matter here?
Normally, if Mammoth sees two consecutive paragraphs with the exact same style, it tries to group them or keep them tightly linked to save space.
However, for headings or specific captions like Image Caption, you almost always want each one to stand entirely on its own as a distinct block element (<h1>).
- Without :fresh: If you had two "Image Caption" paragraphs back-to-back, Mammoth might awkwardly clump them together or treat them under a single closing tag logic.
- With :fresh (h1:fresh): It guarantees that every single time Mammoth encounters an "Image Caption" paragraph in Word, it creates a completely separate, clean, standalone <h1> tag in your HTML output.
Sources
- mammoth 0.3.9 - PyPI
UP: can i use xpath in firefox dev tools? and where?
Response: Yes, you can use XPath in Firefox Developer Tools, and there are two primary places where you can evaluate or use it:
1. The Inspector Panel (Search Bar)
You can search and highlight elements directly using XPath in the HTML inspector tree:
- Where: Open the Inspector panel.
- How: Press Ctrl + F (Windows/Linux) or Cmd + F (Mac) to open the search bar at the bottom of the panel.
- Usage: Type your XPath expression directly into this search box (e.g., //div[@class='example']). Firefox will find and highlight the matching elements on the page.
2. The Web Console ($x helper)
If you want to evaluate an XPath expression programmatically and return an array of matching DOM elements to interact with:
- Where: Open the Web Console.
- How: Use the built-in command-line helper function $x().
- Usage: Pass your XPath string inside the function, like this: $x("//div/div/label") This will evaluate the expression and output an array of matching elements directly in the console.
Bonus: Copying an XPath
If you just want the XPath of an element without writing it from scratch:
- Right-click any element in the Inspector tree, go to Copy, and select XPath (or Full XPath).
How to find xpath in Firefox
This video provides a quick demonstration of finding and utilizing element XPaths inside browser developer tools.
http://googleusercontent.com/youtube_content/1
Sources
- Evaluate XPath to find elements in the page - DevTools Tips
UP: how to find an element preceded by another element in xpath
Response: To find an element that is preceded by another element in XPath, you use specialized XPath Axes.
Depending on whether the preceding element shares the exact same parent or just exists anywhere earlier in the document tree, choose one of the following two approaches:
1. preceding-sibling (For elements sharing the same parent)
Use this when your target element and the reference element share the same parent container (i.e., they are direct brothers/sisters in the HTML structure).
- Syntax: //target_tag/preceding-sibling::reference_tag
- Example: If you want to select a <label> that comes immediately before an <input> field: //input[@id='email']/preceding-sibling::label (This finds the <input> with ID email, then looks backward at its sibling elements to find the matching <label>.)
2. preceding (For elements anywhere earlier in the document)
Use this when the target element you want to find is anywhere before your reference element in the entire HTML structure, even if they don't share the same parent.
- Syntax: //reference_tag/preceding::target_tag
- Example: If you want to find the closest heading (<h2>) that appears anywhere before a specific paragraph: //p[@id='target-para']/preceding::h2[1] (Note: Adding [1] ensures you grab the nearest preceding element rather than the very first one at the top of the page.)
Sources
- preceding-sibling XPath: The Ultimate Guide for Testers - MuukTest
UP: can u use xquery in beatuifulsoup query
Response: No, you cannot use XPath queries natively inside BeautifulSoup. BeautifulSoup uses its own Pythonic methods (find(), find_all()) and supports CSS selectors (select()), but it does not have an internal engine to parse or evaluate XPath syntax.
However, because BeautifulSoup relies heavily on external parsers like lxml, you can easily bridge them if you specifically need XPath.
How to use XPath alongside BeautifulSoup
You can pass your HTML or a BeautifulSoup element through lxml to evaluate your XPath, and then bring the results back into BeautifulSoup:
from bs4 import BeautifulSoup
from lxml import etree
# 1. Load your HTML with BeautifulSoup as usual
html_content = "<div><h1>Heading</h1><p class='c1'>Text</p></div>"
soup = BeautifulSoup(html_content, 'html.parser')
# 2. Convert the BeautifulSoup object (or a sub-element) into an lxml element tree
dom = etree.HTML(str(soup))
# 3. Now you can use XPath queries on that lxml tree
matches = dom.xpath("//p[@class='c1']")
for match in matches:
# If you want to hand it back to BeautifulSoup for further processing:
bs_element = BeautifulSoup(etree.tostring(match), 'html.parser')
print(bs_element.text)
Alternative: Use BeautifulSoup's built-in CSS Selectors
Instead of mixing libraries for XPath, remember that BeautifulSoup natively supports CSS selectors via .select(), which can handle many of the same hierarchical queries:
- XPath: //div[@class='warning']//p
- BeautifulSoup equivalent: soup.select("div.warning p")
Sources
- Can I Use XPath Selectors in BeautifulSoup? - Bright Data
- Can I use XPath selectors in BeautifulSoup? - ScrapingBee
UP: how to select an element in BeautifulSoup preceded by another element?
Response: In BeautifulSoup, you cannot use standard CSS selectors to find an element preceded by another (as CSS lacks a native "previous sibling" combinator, unlike XPath's preceding-sibling).
Instead, BeautifulSoup provides built-in navigation methods to look backward in the document tree.
1. Find the immediate previous sibling
If you want to find the element that directly comes before a specific reference element, use find_previous_sibling():
from bs4 import BeautifulSoup
html = "<div><label>Username:</label><input type='text' id='user'/></div>"
soup = BeautifulSoup(html, "html.parser")
# Find the input element
input_el = soup.find("id", id="user") # or soup.find("input")
# Get the immediate preceding sibling
prev_element = input_el.find_previous_sibling()
print(prev_element) # Output: <label>Username:</label>
You can also pass filters into it if you are looking for a specific tag name:
# Find the previous sibling only if it's a label
label_el = input_el.find_previous_sibling("label")
2. Find all preceding elements
If the element you want is further up and not an immediate sibling, or if you want to search through all preceding elements in the document, use find_previous() or find_all_previous():
# Find the closest preceding paragraph anywhere before the target
target = soup.find("div", class_="content")
prev_p = target.find_previous("p")
# Or get a list of all preceding elements matching a tag
all_prev_headers = target.find_all_previous("h2")
Sources
- Is it possible to select preceding siblings using CSS selectors? - Scrapfly Blog
- Beautiful Soup - find_previous_sibling() Method - TutorialsPoint
- How to find sibling HTML nodes using BeautifulSoup and Python? - ScrapingBee
XSLT
UP: I want xslt transform my html file. There is a div.imageCaption>p and neighboring (right before or after) p>img. My task is to move the img inside the div.imageCaption
Response: To solve this in XSLT, you can use the standard Identity Template (which copies everything through unchanged by default) combined with two specific rules:
- Match the div with the class imageCaption, copy it, and explicitly pull in its neighboring p/img (whether it's sitting right before or right after it).
- Match that target p containing the img in its original location and suppress (delete) it so you don't end up with duplicate images.
Here is an XSLT 1.0/2.0 compatible stylesheet that handles both scenarios (whether the image is a following sibling or a preceding sibling):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:copy-of select="following-sibling::p[1]/img"/>
<xsl:copy-of select="preceding-sibling::p[1]/img"/>
</xsl:copy>
</xsl:template>
<xsl:template match="p[img]">
</xsl:template>
</xsl:stylesheet>
How it works:
- following-sibling::p[1]/img looks immediately after the div to see if there is a paragraph carrying an image, and copies it inside.
- preceding-sibling::p[1]/img looks immediately before the div for the exact same thing.
- match="p[img]" catches that specific paragraph container in its old spot and eats it (by providing an empty template body), ensuring the image successfully "moves" rather than copies.
UP: I don't wanna delete the p element containing img. Leave it and only move img away
Response: To leave the original <p> element intact while extracting and moving the <img> inside your div.imageCaption, you simply need to remove the deletion rule for p[img] and let the identity template handle copying the empty paragraph normally.
Here is the updated XSLT stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:copy-of select="following-sibling::p[1]/img"/>
<xsl:copy-of select="preceding-sibling::p[1]/img"/>
</xsl:copy>
</xsl:template>
<!-- NOTE: The rule matching p[img] has been removed,
so the <p> will stay behind, and the identity template
will copy it (though its <img> child will be extracted out). -->
</xsl:stylesheet>
What happens in this version:
- The div.imageCaption grabs a copy of the <img> from its neighboring <p> and places it inside itself.
- Because the instruction to delete p[img] is gone, the original <p> tag is safely preserved in the document tree.
UP: it will leave a copy of the img in the p element, won't it
Response: You are completely right! Good catch.
Because xsl:copy-of creates a completely new duplicate of the node, and the identity template still processes the original <p> element (including its child <img>), you would end up with two copies of the image.
To truly move it (meaning it leaves the original <p> element behind, but without its <img> child), you need to tell the identity template to copy the <p> except for its <img> child.
Here is the corrected XSLT stylesheet that achieves a true move:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:copy-of select="following-sibling::p[1]/img"/>
<xsl:copy-of select="preceding-sibling::p[1]/img"/>
</xsl:copy>
</xsl:template>
<xsl:template match="p[img][following-sibling::div[1][@class='imageCaption'] | preceding-sibling::div[1][@class='imageCaption']]">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="node()[not(self::img)]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
What changed:
- The second rule intercepts the specific neighboring <p> tag that touches the div.imageCaption.
- It uses node()[not(self::img)] to copy all text and other children of that paragraph except the image, effectively stripping the image out of its original home so it lives exclusively inside the div.imageCaption.
UP: remind me how to apply xsl code to html
Response: Applying an XSLT stylesheet to an HTML (or XHTML) file can be done directly inside your Python pipeline using the lxml library, which you may already have installed since it powers BeautifulSoup under the hood.
Here is how you execute an XSLT transformation script on a file in Python:
from lxml import etree
# 1. Load your XSLT stylesheet
xslt_path = "move_image.xsl"
xslt_doc = etree.parse(xslt_path)
transform = etree.XSLT(xslt_doc)
# 2. Load your target HTML file
# (Note: Use html parser to safely handle standard messy HTML tags)
html_path = "output_article.html"
html_parser = etree.HTMLParser()
html_tree = etree.parse(html_path, html_parser)
# 3. Apply the XSLT transformation
result_tree = transform(html_tree)
# 4. Save the transformed result back to a file
output_path = "output_article_transformed.html"
with open(output_path, "wb") as f:
f.write(bytes(result_tree))
print(f"Transformation complete! Saved to {output_path}")
Key things to remember when using XSLT on HTML:
- Parser Choice: Always use etree.HTMLParser() when reading the HTML file rather than etree.XMLParser(). HTML is often missing closing tags or has loose syntax that will crash a strict XML parser.
- Output Serialization: Converting the result_tree back via bytes(result_tree) respects whatever output serialization settings you declared at the top of your XSLT file (such as <xsl:output method="html" encoding="utf-8"/>).
How to convert XML into HTML using XSLT 2 transformation from Python3
This video demonstrates a workflow for setting up XSLT transformation scripts and executing them through Python code.
UP: but why result of the conversion is in some different encoding. The characters don't render correctly?
Response: Character rendering issues after an XSLT transformation usually happen because of a mismatch between how lxml serializes the output bytes and how you are writing them to the disk, or because the XSLT processor defaults to an ASCII or system-default encoding.
When you do bytes(result_tree), lxml follows the encoding rules specified in your XSLT stylesheet. If no encoding is specified, it might default to ASCII or lose the UTF-8 flag, causing special characters, accented letters, or symbols to break into strange symbols (mojibake).
Fix 1: Declare UTF-8 explicitly inside your XSLT
Add an <xsl:output> tag near the top of your XSLT file (right under <xsl:stylesheet>) to force it to serialize using UTF-8:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="utf-8" indent="yes"/>
</xsl:stylesheet>
Fix 2: Write using explicit UTF-8 string conversion in Python
Instead of relying on bytes(result_tree) which can sometimes behave unpredictably depending on the system environment, convert the transformation result directly into a Python Unicode string (str) and write it using standard Python file writing with encoding="utf-8":
from lxml import etree
# 1. Load XSLT and HTML
xslt_doc = etree.parse("move_image.xsl")
transform = etree.XSLT(xslt_doc)
html_parser = etree.HTMLParser()
html_tree = etree.parse("output_article.html", html_parser)
# 2. Transform
result_tree = transform(html_tree)
# 3. Convert result to a Python unicode string safely
result_string = str(result_tree)
# 4. Write to file explicitly using UTF-8 encoding
output_path = "output_article_transformed.html"
with open(output_path, "w", encoding="utf-8") as f:
f.write(result_string)
print("Saved cleanly with correct encoding!")
Why this fixes it:
- Setting encoding="utf-8" in <xsl:output> tells the XSLT engine how to map the characters.
- Converting via str(result_tree) combined with Python's open(..., "w", encoding="utf-8") guarantees that even if your terminal or OS default encoding is set to something older (like cp1252 or ASCII), the file itself on your hard drive will be safely encoded in standard UTF-8.
Sources
- XPath and XSLT with lxml
UP: didn't help
Response: If the characters are still garbled, the issue is almost certainly happening at the very first step: when lxml reads your source HTML file.
If you use etree.parse("file.html") without telling lxml what encoding the source file is in, lxml defaults to guessing or using ASCII, which silently corrupts non-English characters or special symbols before the XSLT even touches it.
To fix this completely, you need to tell lxml's HTML parser to explicitly read the source file using utf-8, and then write it back out using lxml's native binary write method:
from lxml import etree
# 1. Load your XSLT stylesheet normally
xslt_doc = etree.parse("move_image.xsl")
transform = etree.XSLT(xslt_doc)
# 2. CRITICAL: Force the HTML parser to read the source file as UTF-8
html_parser = etree.HTMLParser(encoding="utf-8")
html_tree = etree.parse("output_article.html", html_parser)
# 3. Apply the XSLT transformation
result_tree = transform(html_tree)
# 4. Write out the result directly as bytes using lxml's built-in write method
# (This respects the <xsl:output encoding="utf-8"/> rule inside your XSL file)
output_path = "output_article_transformed.html"
result_tree.write(output_path, encoding="utf-8", method="html", pretty_print=False)
print("Saved with correct encoding!")
Why this fixes it:
- etree.HTMLParser(encoding="utf-8"): Prevents lxml from misinterpreting special characters when initially opening your raw HTML file.
- result_tree.write(..., encoding="utf-8"): Bypasses Python's string conversion quirks entirely and lets lxml handle the byte-level serialization correctly based on your XSLT settings.
UP: the xsl script removed other p>img elements with no neighboring div.imgCaption
Response: That happened because of how the selector was written in the second rule. It accidentally matched all paragraphs containing images because the sibling check was too loose or misapplied.
To fix this so that only the specific <p><img/></p> element directly next to a div.imageCaption loses its image (while all other images in your document remain completely untouched), we need to tighten the XPath match.
Here is the corrected XSLT stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:copy-of select="following-sibling::p[1][img]/img"/>
<xsl:copy-of select="preceding-sibling::p[1][img]/img"/>
</xsl:copy>
</xsl:template>
<xsl:template match="p[img][following-sibling::div[1][@class='imageCaption'] or preceding-sibling::div[1][@class='imageCaption']]">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="node()[not(self::img)]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
What changed to fix it:
- Strict Sibling Matching: The check now explicitly looks for following-sibling::div[1][@class='imageCaption'] (meaning the very next sibling is that specific div) or the reverse for the preceding sibling.
- Untouched Images: Any other <p><img/></p> elements in your document that are sitting next to regular text paragraphs or other tags will fail this strict check and fall through to the standard identity template, leaving them completely safe and untouched.
UP: still removes other imgs
Response: The issue happens because absolute XPath axes like following-sibling::div[1] can behave unexpectedly depending on whitespace/text nodes in HTML, accidentally matching paragraphs that aren't strictly adjacent to your target class.
Instead of navigating using complex sibling axes that easily misfire, a much safer and cleaner approach is to use XSLT Keys or check if the img's immediate parent paragraph is directly next to the caption div.
Here is a foolproof XSLT script that targets only the specific image belonging to that precise relationship, leaving all other images completely untouched:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:if test="preceding-sibling::*[1][self::p]/img">
<xsl:copy-of select="preceding-sibling::*[1]/img"/>
</xsl:if>
<xsl:if test="following-sibling::*[1][self::p]/img">
<xsl:copy-of select="following-sibling::*[1]/img"/>
</xsl:if>
</xsl:copy>
</xsl:template>
<xsl:template match="p[img]/following-sibling::div[1][@class='imageCaption']/preceding-sibling::p[1]/img | div[@class='imageCaption']/preceding-sibling::*[1][self::p]/img">
</xsl:template>
</xsl:stylesheet>
Why this version works safely:
- preceding-sibling::*[1][self::p]: This strictly checks that the very first previous sibling element is a paragraph (p) and nothing else (ignoring whitespace text nodes).
- Targeted Deletion: The second template explicitly targets only the specific img node that matches the move condition, deleting it from its source paragraph while leaving every other standalone <img> across the rest of your document completely safe.
UP: I want to add a special attribute to markup the p/img nodes which were moved into div.imgCaption
Response: To add a special attribute (like data-moved="true" or class="moved-image") to the <img> node when it is moved into the div.imageCaption, you can use an XSLT variable or directly construct the element with <xsl:element> / <xsl:attribute>.
Here is the updated XSLT script that pulls the image into the div.imageCaption and automatically injects your desired custom attribute:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:for-each select="preceding-sibling::*[1][self::p]/img">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:attribute name="data-moved">true</xsl:attribute>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:for-each>
<xsl:for-each select="following-sibling::*[1][self::p]/img">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:attribute name="data-moved">true</xsl:attribute>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="p[img]/following-sibling::div[1][@class='imageCaption']/preceding-sibling::p[1]/img | div[@class='imageCaption']/preceding-sibling::*[1][self::p]/img">
</xsl:template>
</xsl:stylesheet>
What this does:
- It uses <xsl:for-each> combined with <xsl:copy> to recreate the <img> tag inside the div.imageCaption.
- It preserves all original attributes (src, alt, class, etc.) using <xsl:copy-of select="@*"/>.
- It explicitly injects your custom markup using <xsl:attribute name="data-moved">true</xsl:attribute> (you can change data-moved to whatever attribute name or value you prefer).
UP: I need Adding an attribute to the source img node, not the target. In other words The img from which the div.imgCaption/img is being copied. After marking it is will be able to identify it and remove
Response: To add a special attribute to the source <img> node before it gets moved or while it's being processed, you can target the source image directly using a matching template rule.
Instead of trying to delete it blindly at the end, you can match the source image, inject your marker attribute (data-to-remove="true" or similar), and then handle the move or deletion cleanly.
Here is the updated XSLT script that marks the source <img> first:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:copy-of select="preceding-sibling::*[1][self::p]/img"/>
<xsl:copy-of select="following-sibling::*[1][self::p]/img"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']/preceding-sibling::*[1][self::p]/img | p[img]/following-sibling::div[1][@class='imageCaption']/preceding-sibling::p[1]/img">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:attribute name="data-marked-for-removal">true</xsl:attribute>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
How this works:
- The Marker Rule: The second template specifically isolates the <img> sitting inside the neighboring paragraph that touches your div.imageCaption. It copies it and injects <xsl:attribute name="data-marked-for-removal">true</xsl:attribute>.
- Identification: When the transformation finishes, that specific source image will now have data-marked-for-removal="true" right where it originally lived.
- Post-Processing / Removal: If you want Python or a subsequent step to remove it, you can easily find it using BeautifulSoup later (soup.find_all('img', attrs={'data-marked-for-removal': 'true'}) and call .decompose() on them), or you can change the template body to do nothing if you want XSLT to wipe it out completely after marking.
UP: Div[@class= 'imageCaption']/preceding-sibling::*[1][self::p]/img | p[img]/following -sibling::div[1][@class='imageCaption'] /preceding-sibling::p[1]/img Explain
Response: This complex XPath expression is a union (using the | pipe operator) of two separate paths designed to pinpoint the exact source <img> element sitting in a neighboring paragraph right next to a div[@class='imageCaption'].
Let's break down each half of the expression step-by-step:
Part 1: div[@class='imageCaption']/preceding-sibling::*[1][self::p]/img
This part looks backward from the div.imageCaption to find an image in the paragraph immediately preceding it.
- div[@class='imageCaption']: Start at a div element whose class is imageCaption.
- /preceding-sibling::*[1]: Look at the very first sibling immediately before this div (using *[1] ensures we only grab the closest neighbor and skip text nodes or whitespace).
- [self::p]: A filter (predicate) that checks: "Is that preceding sibling actually a <p> element?" If it's something else (like an <h2> or a standard <div>), this match fails.
- /img: Look inside that confirmed neighboring paragraph and select its child <img> element.
Part 2: p[img]/following-sibling::div[1][@class='imageCaption']/preceding-sibling::p[1]/img
This part approaches the relationship from the other direction (looking forward), ensuring we catch cases where the image is packaged in a specific structural layout.
- p[img]: Start at any paragraph that contains an image.
- /following-sibling::div[1][@class='imageCaption']: Look at the very next sibling of that paragraph to see if it is a div with the class imageCaption.
- /preceding-sibling::p[1]: Step back from that div to find the immediately preceding paragraph. (Because we started at a paragraph, this loops back to confirm the relationship is tight and reciprocal).
- /img: Finally, select the <img> element inside that target paragraph.
Summary of Purpose
The | combines both paths into a single unified selector. It acts as an absolute safeguard: it guarantees that only the <img> tags belonging to a paragraph directly touching a div.imageCaption are matched, preventing the XSLT stylesheet from accidentally modifying or touching any other images elsewhere in your HTML document.
UP: When transforming, xslt processor takes output of preceding template as an input for the next one, am I right
Response: No, that is a common misconception! XSLT does not work like a pipeline or a procedural script where instructions run step-by-step, passing data down a chain.
Instead, XSLT is a declarative, rule-based language.
How XSLT Actually Works:
- The Source Tree is Read-Only: The input HTML file is loaded into memory as a static tree structure. The processor never "mutates" or changes the input tree as it goes.
- Global Pattern Matching: When you run <xsl:apply-templates>, the processor looks at the current node and scans all of your templates simultaneously to see which one has a match pattern that fits best.
- Template Independence: Templates don't pass output to one another. Each template matches a specific node, transforms it, and writes its output directly to the result tree (the final output document being built).
Why did you write two templates then?
The reason you have multiple templates is for priority and specialization, not sequencing:
- The Identity Template (match="node()|@*"): Acts as a catch-all safety net. It matches everything in your document and copies it by default.
- Your Specific Templates (match="div[@class='imageCaption']" etc.): Because they are more specific than the catch-all identity template, the XSLT processor gives them higher priority. When the processor encounters a div.imageCaption, it temporarily overrides the identity template for that specific node to perform your custom logic, then goes back to normal copying for everything else.
UP: <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="node()|@*"> <xsl:copy> <xsl:apply-templates select="node()|@*"/> </xsl:copy> </xsl:template> <xsl:template match="div[@class='imageCaption']"> <xsl:copy> <xsl:apply-templates select="node()|@*"/> <xsl:copy-of select="preceding-sibling::*[1][self::p]/img"/> <xsl:copy-of select="following-sibling::*[1][self::p]/img"/> </xsl:copy> </xsl:template> <xsl:template match="div[@class='imageCaption']/preceding-sibling::*[1][self::p]/img | p[img]/following-sibling::div[1][@class='imageCaption']/preceding-sibling::p[1]/img"> <xsl:copy> <xsl:copy-of select="@*"/> <xsl:attribute name="data-marked-for-removal">true</xsl:attribute> </xsl:copy> </xsl:template> </xsl:stylesheet> How the logic flows
Response: To understand how this XSLT script executes, you have to throw away the idea of a top-to-bottom procedural script. Instead, think of it as a declarative rulebook that the XSLT processor consults as it walks through your HTML document tree.
Here is step-by-step how the logic flows when you run this script:
Phase 1: The Document Walkthrough (The Identity Template)
- The XSLT processor starts at the root of your HTML file and visits every single node (elements, attributes, text) one by one.
- For most nodes (like standard paragraphs, headings, or random divs), the processor looks for a matching template.
- Since generic nodes don't match your specialized templates, they fall back to the Identity Template (match="node()|@*"), which simply copies them as-is to the output tree and tells the processor to keep walking (<xsl:apply-templates>).
Phase 2: Encountering a div[@class='imageCaption']
- When the processor hits a div with the class imageCaption, it finds a more specific match than the identity template: your first custom template (match="div[@class='imageCaption']").
- Inside this template, the processor executes instructions only for this specific div:
- <xsl:apply-templates select="node()|@*"/>: Copies any existing children already inside the div (like text or spans) into the output.
- <xsl:copy-of select="preceding-sibling::*[1][self::p]/img"/>: Looks backward. If the immediately preceding sibling is a paragraph containing an image, it clones that image and injects a copy right here inside the div.
- <xsl:copy-of select="following-sibling::*[1][self::p]/img"/>: Does the exact same check looking forward.
Phase 3: Encountering the Source <img> Element
Meanwhile, somewhere else in the document tree (or during the traversal), the processor eventually visits the original <img> tag sitting inside that neighboring <p> element.
- The processor checks your templates to see how to handle this specific <img>.
- It finds your second template because its complex XPath matches this exact node: div[@class='imageCaption']/preceding-sibling::*[1][self::p]/img (or its reverse).
- Instead of letting the identity template copy it normally, this specific template intercepts it:
- It copies the image (<xsl:copy>).
- It preserves all of the image's original attributes like src or alt (<xsl:copy-of select="@*"/>).
- It injects your custom marker attribute: <xsl:attribute name="data-marked-for-removal">true</xsl:attribute>.
Summary of the Final Result
When the transformation finishes, your output document contains:
- The Target: The div.imageCaption now contains its original content plus a newly copied <img>.
- The Source: The original <img> left behind in its paragraph is still there, but it now proudly wears data-marked-for-removal="true", allowing your Python script to easily find and delete it.
UP: xsltCompilePattern : failed to compile 'p[img]/following-sibling::div[1][@class='imageCaption']/preceding-sibling::p[1]/img '
Response: This error occurs because XSLT match patterns (the strings you put inside match="...") have stricter rules than general XPath expressions.
In XSLT 1.0, standard processors (like libxslt which Python's lxml uses) do not allow forward-looking axes like following-sibling directly inside a template match attribute. They only allow backward navigation or structural parent-child tests.
The Fix
Instead of trying to match the source image using complex forward/backward sibling checks in the match attribute, let the Identity Template copy the image normally, and handle the moving/marking logic entirely inside the div.imageCaption template.
Here is the clean, error-free XSLT script that avoids invalid match patterns entirely:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:for-each select="preceding-sibling::*[1][self::p]/img">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:attribute name="data-moved">true</xsl:attribute>
</xsl:copy>
</xsl:for-each>
<xsl:for-each select="following-sibling::*[1][self::p]/img">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:attribute name="data-moved">true</xsl:attribute>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="p[img][following-sibling::*[1][self::class='imageCaption'] or preceding-sibling::*[1][@class='imageCaption']]">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="node()[not(self::img)]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Why this works:
- It eliminates the illegal following-sibling syntax from the match attribute, so libxslt compiles it successfully without crashing.
- It uses xsl:for-each to grab the image, stamp your marker attribute onto the cloned version inside the caption div, and safely strips the original image out of its source paragraph in one clean pass.
UP: only p/img with preceding imageCaption div worked. Ones with following imageCaption div failed to get rid of img
Response: The reason the paragraphs following the image caption div failed to strip their images is because of how XSLT handles lookaheads in template matching.
When a <p> tag appears before a div.imageCaption, its following sibling is that div. However, XSLT template patterns checking following-sibling can fail or drop out depending on node positioning because of how the parser evaluates downward text/element boundaries.
The Robust Fix
Instead of relying on complicated match patterns to strip the image from the paragraph, let's invert the logic completely.
Instead of telling the paragraph to strip its image, we can tell the div.imageCaption to pull the image and simultaneously use an xsl:choose block to safely clear out the image from whichever neighboring paragraph it came from (whether preceding or following).
Here is the fully corrected, reliable XSLT stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:for-each select="preceding-sibling::*[1][self::p]/img">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:attribute name="data-moved">true</xsl:attribute>
</xsl:copy>
</xsl:for-each>
<xsl:for-each select="following-sibling::*[1][self::p]/img">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:attribute name="data-moved">true</xsl:attribute>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="p[img]">
<xsl:choose>
<xsl:when test="following-sibling::*[1][@class='imageCaption']">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="node()[not(self::img)]"/>
</xsl:copy>
</xsl:when>
<xsl:when test="preceding-sibling::*[1][@class='imageCaption']">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="node()[not(self::img)]"/>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Why this fixes the issue:
- Simplified Matching: Instead of an uncompiled, fragile compound match string, template #2 now simply matches all paragraphs containing images (match="p[img]").
- Explicit Directional Check: Inside, it uses an <xsl:choose> to check safely whether a div.imageCaption is sitting directly before or directly after it.
- Guaranteed Stripping: Whichever direction it finds the caption div, it cleanly copies the paragraph text while omitting the <img>, ensuring both preceding and following layouts work identically.
UP: Please explain the logic flow of the code.
Response: Here is the step-by-step logic flow of the final XSLT script as it processes your HTML document:
Step 1: The Global Safety Net (Identity Template)
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
- What happens: The XSLT processor scans the entire HTML document tree node by node. By default, any element, attribute, or text that doesn't trigger a more specific rule falls back to this template, which copies it directly to the output tree unchanged.
Step 2: Encountering div[@class='imageCaption']
When the processor reaches your special caption div, it intercepts it using the first custom template:
<xsl:template match="div[@class='imageCaption']">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
<xsl:for-each select="preceding-sibling::*[1][self::p]/img">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:attribute name="data-moved">true</xsl:attribute>
</xsl:copy>
</xsl:for-each>
<xsl:for-each select="following-sibling::*[1][self::p]/img">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:attribute name="data-moved">true</xsl:attribute>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
- What happens:
- The div copies its existing contents.
- It checks its immediate neighbor (either behind it or ahead of it). If that neighbor is a paragraph containing an image, it clones that image, stamps the data-moved="true" attribute onto the clone, and injects it directly inside the div.
Step 3: Encountering Any Paragraph with an Image (p[img])
When the processor encounters a paragraph containing an image anywhere else in the document, it hits the second template and evaluates it through an <xsl:choose> conditional block:
<xsl:template match="p[img]">
<xsl:choose>
<xsl:when test="following-sibling::*[1][@class='imageCaption']">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="node()[not(self::img)]"/>
</xsl:copy>
</xsl:when>
<xsl:when test="preceding-sibling::*[1][@class='imageCaption']">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="node()[not(self::img)]"/>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
- What happens:
- Case A & B: If the paragraph is directly touching a div.imageCaption (whether the div is ahead of it or behind it), the processor recreates the paragraph shell, copies all of its text/content, but intentionally omits ([not(self::img)]) the <img> tag. This successfully strips the original image out of its source home.
- Case C: If it's just a regular paragraph somewhere else in the article unrelated to a caption block, it falls back to <xsl:otherwise> and gets copied completely untouched.
UP: does MediaWiki support img html tag or I'm to convert it to wiki text
Response: You must convert the <img> tags to MediaWiki wikitext syntax.
MediaWiki does not natively support or safely parse raw HTML <img> tags in standard wikitext. Even in older versions where a specific config flag existed to permit it ($wgAllowImageTag), it has been completely removed due to security and privacy risks.
Instead, you need to transform your HTML image structures into standard MediaWiki file syntax:
1. Basic Conversion Format
Standard MediaWiki image syntax looks like this:
[[File:Filename.jpg|thumb|Alt text or caption]]
2. Handling your specific layout (div.imageCaption + <img>)
Since you are moving the image inside an element with captions or specific styling classes, MediaWiki handles thumbnails, framing, and captions automatically via its built-in parameters.
For example, if you have an image with a caption, you typically convert it to:
[[File:example.jpg|thumb|Your caption text goes here]]
- thumb (or thumbnail): Automatically generates a framed box that floats to the side (usually right) and places your text description at the bottom as a caption.
- left / right / center: Controls alignment.
- 100px: Controls the scaling width.
Sources
- lihaohong6/ImgTag - GitHub
- Manual:$wgAllowImageTag - MediaWiki
- Formatting images in MediaWiki - InMotion Hosting
UP: there are 3 types of p/strong elements: 1) containing all capital unicode chars like <strong>ФЫАЫФА</strong>. this is actually an article title; 2) first letter capital like <strong>Авааф фыаа</strong>. This is a section header inside an article; 3) all letters small like <strong>выфафыа</strong>. This is just an inline bold text having no structural load. As a long term goal I need to classify all of them in my html document. As a specific quetion: can I use regex pattern inside XPath expression to match all capitals and first letter capital text patterns?
Response: No, you cannot use native regular expressions directly inside standard XPath 1.0 expressions.
Standard XPath 1.0 (which is the version built into Python's lxml library) does not support regex features, pattern matching flags, or functions like fn:matches(). Those tools only arrived later in XPath 2.0+.
However, since you are already processing your HTML via Python, you have two great ways to achieve your long-term classification goal:
Approach 1: Let Python handle the Regex classification (Recommended)
Instead of forcing XPath to do complex string parsing, let XSLT/XPath handle the structural matching (finding the <strong> elements), and then pass the text content to Python's built-in re module for precise classification.
import re
from lxml import etree
html_content = """
<div>
<p><strong>ФЫАЫФА</strong></p>
<p><strong>Авааф фыаа</strong></p>
<p><strong>выфафыа</strong></p>
</div>
"""
# Compile your regex patterns for the 3 types
# Note: Using Python's regex supports full Unicode character classes (\w, \p, etc.)
re_all_caps = re.compile(r"^\W*[\p{Lu}\s]+\W*$", re.UNICODE) # roughly: all uppercase letters
# Simpler approach in standard Python: check if string equals its own uppercase version and has letters
def classify_strong_text(text):
text = text.strip()
if not text:
return "other"
# Check if it has letters and is entirely uppercase
if text == text.upper() and text != text.lower():
return "type_1_article_title"
# Check if first letter is uppercase, and rest has lowercase or is mixed
elif text[0].isupper() and text[0] == text.upper() and any(c.islower() for c in text):
return "type_2_section_header"
# Otherwise all lowercase / inline text
else:
return "type_3_inline_bold"
# Parse with lxml
parser = etree.HTMLParser(encoding="utf-8")
tree = etree.fromstring(html_content, parser)
# Find all strong elements using a simple, clean XPath
strong_elements = tree.xpath("//strong")
for el in strong_elements:
text = el.text or ""
classification = classify_strong_text(text)
print(f"Text: '{text}' --> Classification: {classification}")
# You can even inject a classification attribute back into the XML element dynamically!
el.set("data-text-type", classification)
Approach 2: Use EXSLT Regular Expressions in lxml (If XPath is mandatory)
If you specifically need the filtering to happen inside the XPath query string, lxml supports EXSLT (a community extension that adds regex support via regexp:match to XPath 1.0).
You have to register the namespace in Python to use it:
from lxml import etree
# EXSLT regex namespace extension
ns = {"re": "http://exslt.org/regular-expressions"}
html_tree = etree.HTMLParser(encoding="utf-8")
tree = etree.fromstring(html_content, html_tree)
# Find elements whose text matches an all-caps pattern using EXSLT regex
# (Matches strings consisting entirely of uppercase letters and spaces)
all_caps_nodes = tree.xpath("//strong[re:match(text(), '^[\\p{{Lu\\s}}]+$', 'u')]", namespaces=ns)
Note: EXSLT regex syntax can sometimes behave inconsistently with complex international Unicode blocks depending on the underlying libxml2 system binary compiled on your OS, which is why Approach 1 (Python-side evaluation) remains vastly more reliable for multi-lingual text.
UP: explain re:match(text(), '^[\\p{{Lu\\s}}]+$', 'u')
Response: The EXSLT expression re:match(text(), '^[\\p{{Lu\\s}}]+$', 'u') is used to test whether the text inside an element consists entirely of uppercase letters and whitespace.
Here is a breakdown of every piece of that expression:
1. re:match(node, pattern, flags)
- re:: This is the namespace prefix telling the processor to use EXSLT's regular expression library instead of standard XPath.
- text(): Passes the text content of the current element into the function.
- '^[\\p{{Lu\\s}}]+$': The regular expression pattern string.
- 'u': The flag parameter. Here, 'u' stands for Unicode mode, which ensures the regex engine understands international character sets (like Cyrillic, Greek, etc.) rather than just basic English ASCII. (Note: Because this is written inside an XPath string literal, curly braces are doubled {{ and }} to escape them).
2. The Regex Pattern Breakdown: ^[\\p{{Lu\\s}}]+$
- ^: Asserts the start of the string.
- $: Asserts the end of the string. (Together with ^, this forces the match to cover the entire text from beginning to end, rather than just finding a match hiding somewhere inside a longer sentence).
- [...]: A character class matching any single character contained inside the brackets.
- \\p{Lu}: The Unicode property escape for Letter, uppercase (all capital letters across any alphabet, including Cyrillic like ФЫА).
- \\s: Matches any whitespace character (spaces, tabs, newlines).
- +: A quantifier meaning "one or more times".
Summary
Combined together, ^[\\p{{Lu\\s}}]+$ means: "From the absolute beginning to the absolute end of this string, every single character must be either an uppercase letter or a space."
UP: can I use the same XPath with regex inside firefox dev tools
Response: No, you cannot use that expression directly in Firefox DevTools.
There are two main roadblocks if you try it:
- The Inspector Search Bar (Ctrl+F): Firefox's built-in search input in the Inspector panel only supports basic, standard XPath 1.0 queries. It does not support EXSLT extensions (re:match) or namespaces, so it will throw an syntax error.
- The Browser Console ($x(...)): While Firefox's JavaScript console supports XPath via the $x("//strong") helper function, it relies on native browser DOM evaluation. Browsers do not natively implement EXSLT regex matching inside standard DOM XPath evaluations.
Alternative: How to test similar queries in Firefox Console
If you want to query elements using regular expressions directly inside the Firefox Developer Tools Console, you should bypass XPath entirely and use standard JavaScript with native RegExp, which fully supports Unicode flags (/u):
// Find all <strong> elements and filter them using a JavaScript regex
Array.from(document.querySelectorAll('strong')).filter(el => {
const text = el.textContent.trim();
// Example: Match all-caps unicode text
return /^[\p{Lu}\s]+$/u.test(text);
});
This snippet gives you the exact same result in the Firefox console while leveraging JavaScript's robust built-in Unicode regex engine.
UP: sometimes <strong> element's text content ends with non letter char(s). this should be by passed
Response: To handle trailing non-letter characters (like punctuation, periods, commas, colons, or quote marks) so they don't break your classification rule, you can update your regex pattern or Python filtering logic to ignore or bypass trailing punctuation at the end of the string.
Here is how you can handle it using both methods:
1. The Python Approach (Recommended)
If you are classifying the text using Python, you can either strip trailing punctuation before testing, or adjust your conditions:
import re
def classify_strong_text(text):
# Strip whitespace and trailing/leading non-letter punctuation
# This leaves just the core letters and inner spaces
cleaned_text = text.strip()
if not cleaned_text:
return "other"
# Check if all letters in the string are uppercase
letters = [c for c in cleaned_text if c.isalpha()]
if not letters:
return "other"
# Check if every letter found is uppercase
if all(c.isupper() for c in letters):
# If it's a single word or short phrase all caps, or matches title criteria:
return "type_1_article_title"
# Check if first letter is uppercase and there are lowercase letters
if cleaned_text[0].isupper() and any(c.islower() for c in cleaned_text):
return "type_2_section_header"
return "type_3_inline_bold"
2. The JavaScript/Firefox Console Approach
If you are testing this in the Firefox DevTools console, you can update your regular expression or string trimming to allow trailing punctuation (like ., :, !, or ?) at the end:
Array.from(document.querySelectorAll('strong')).filter(el => {
const text = el.textContent.trim();
// Regex breakdown:
// ^[\p{Lu}\s]+ -> Starts with one or more uppercase letters/spaces
// [^\p{L}]*$ -> Followed optionally by zero or more non-letter characters (punctuation) at the end
const allCapsWithPunctuation = /^[\p{Lu}\s]+[^\p{L}]*$/u;
return allCapsWithPunctuation.test(text);
});
3. The EXSLT XPath Approach (lxml in Python)
If you are using EXSLT regex inside your Python script, you can modify the pattern to make trailing non-letters optional using [^\p{L}]*:
# Matches all-caps text, optionally ending with punctuation marks
pattern = '^[\\p{{Lu\\s}}]+[^\\p{{L}}]*$'
all_caps_nodes = tree.xpath(f"//strong[re:match(text(), '{pattern}', 'u')]", namespaces=ns)
UP: it turn out my strong texts may contain punctuations not only at the end but also inside the text
Response: When punctuation can appear anywhere—inside, at the end, or mixed with spaces and letters—relying strictly on a pure regex match can become messy.
The most robust and maintainable way to handle this in Python is to extract and inspect only the letters inside the string, ignoring all punctuation and whitespace entirely.
Here is how you can update your classification function to safely ignore internal, leading, and trailing punctuation:
The Python Approach (Bulletproof for Mixed Punctuation)
def classify_strong_text(text):
text = text.strip()
if not text:
return "other"
# Extract only the actual letters from the text (supports all Unicode alphabets)
letters = [c for c in text if c.isalpha()]
# If there are no letters at all (e.g., just symbols like "---"), it's not a header/title
if not letters:
return "other"
# 1. TYPE 1: All letters are uppercase (e.g., "ФЫА. ЫФА!", "ARTICLE: TITLE")
if all(c.isupper() for c in letters):
return "type_1_article_title"
# Find the very first letter in the string to check case
first_letter = letters[0]
# 2. TYPE 2: First letter is uppercase, and there is at least one lowercase letter
if first_letter.isupper() and any(c.islower() for c in letters):
return "type_2_section_header"
# 3. TYPE 3: Everything else (all lowercase or mixed inline bold text)
return "type_3_inline_bold"
Why this works so well:
- c.isalpha(): Automatically filters out periods, commas, colons, hyphens, brackets, and quotes regardless of whether they are sitting at the beginning, middle, or end of your <strong> text.
- all(c.isupper() for c in letters): Ensures that every single letter in the string is capitalized, ignoring the fact that there might be a colon or a comma splitting up the words.
- Unicode Safe: Built-in Python string methods fully support international alphabets (Cyrillic, Greek, etc.) without needing complex regex flags.
UP: how to use this code with file input and output
Response: To apply this classification logic to a real HTML file, process it using Python's lxml library (which preserves your document structure and handles UTF-8 encoding safely), and save the modified result to a new file, you can use the complete script below.
Python File Processing Script
from lxml import etree
def classify_strong_text(text):
text = text.strip()
if not text:
return "other"
# Extract only the actual letters, ignoring punctuation and spaces everywhere
letters = [c for c in text if c.isalpha()]
if not letters:
return "other"
# Type 1: All letters are uppercase (e.g., "ФЫА. ЫФА!")
if all(c.isupper() for c in letters):
return "type_1_article_title"
# Find the very first letter in the string
first_letter = letters[0]
# Type 2: First letter is uppercase and contains lowercase letters (e.g., "Авааф, фыаа.")
if first_letter.isupper() and any(c.islower() for c in letters):
return "type_2_section_header"
# Type 3: All lowercase or inline mixed text
return "type_3_inline_bold"
def process_html_file(input_path, output_path):
# 1. Read input HTML with explicit UTF-8 encoding
parser = etree.HTMLParser(encoding="utf-8")
tree = etree.parse(input_path, parser)
# 2. Find all <strong> elements and apply classification
strong_elements = tree.xpath("//strong")
for el in strong_elements:
# Get text content safely (including child nodes if any)
text = "".join(el.itertext())
classification = classify_strong_text(text)
# Inject the classification as a custom attribute for tracking/MediaWiki prep
el.set("data-text-type", classification)
# 3. Save the modified tree back to an output HTML file
# We use html=True to ensure proper HTML tag closure and UTF-8 encoding serialization
tree.write(output_path, encoding="utf-8", method="html", include_meta_content_type=True)
print(f"Successfully processed and saved to: {output_path}")
# Example usage:
# process_html_file("input_document.html", "output_classified.html")
What this script does:
- Reads safely: Loads your source file using lxml with UTF-8 encoding enabled to prevent character corruption for non-English texts.
- Classifies dynamically: Loops through every <strong> tag, reads its inner text (handling mixed internal/trailing/leading punctuation via .isalpha()), and determines its type.
- Annotates markup: Automatically injects a data-text-type="..." attribute directly onto the <strong> element in your HTML tree.
- Writes cleanly: Serializes the DOM back into a valid HTML output file ready for your next migration step.
UP: write() got an unexpected keyword argument 'include_meta_content_type'
Response: My apologies! That parameter (include_meta_content_type) belongs to a different XML module and is invalid for lxml's tree writing method.
Here is the corrected, fully working version of the file processing function:
from lxml import etree
def classify_strong_text(text):
text = text.strip()
if not text:
return "other"
# Extract only the actual letters, ignoring punctuation and spaces everywhere
letters = [c for c in text if c.isalpha()]
if not letters:
return "other"
# Type 1: All letters are uppercase (e.g., "ФЫА. ЫФА!")
if all(c.isupper() for c in letters):
return "type_1_article_title"
# Find the very first letter in the string
first_letter = letters[0]
# Type 2: First letter is uppercase and contains lowercase letters (e.g., "Авааф, фыаа.")
if first_letter.isupper() and any(c.islower() for c in letters):
return "type_2_section_header"
# Type 3: All lowercase or inline mixed text
return "type_3_inline_bold"
def process_html_file(input_path, output_path):
# 1. Read input HTML with explicit UTF-8 encoding
parser = etree.HTMLParser(encoding="utf-8")
tree = etree.parse(input_path, parser)
# 2. Find all <strong> elements and apply classification
strong_elements = tree.xpath("//strong")
for el in strong_elements:
text = "".join(el.itertext())
classification = classify_strong_text(text)
# Inject the classification as a custom attribute
el.set("data-text-type", classification)
# 3. Save the modified tree back to an output HTML file safely
tree.write(output_path, encoding="utf-8", method="html")
print(f"Successfully processed and saved to: {output_path}")
# Example usage:
# process_html_file("input_document.html", "output_classified.html")
UP: character encoding needs to be declared in the Content-Type HTTP header, using a meta tag. how to do this in html
Response: To properly declare the character encoding in your HTML output file so browsers and parsers automatically recognize it as UTF-8, you need to include a standard <meta> tag inside the <head> section of your HTML document.
The Standard HTML5 Meta Tag
Add this line directly under the <head> tag of your HTML document:
<meta charset="utf-8">
How to add it automatically via Python (lxml)
If your input HTML file doesn't have a <head> tag or a charset declaration, you can use Python to automatically inject it during your file processing script before saving.
Here is how you can update your Python script to ensure the UTF-8 meta tag is always present in the output:
from lxml import etree
def process_html_file(input_path, output_path):
parser = etree.HTMLParser(encoding="utf-8")
tree = etree.parse(input_path, parser)
# 1. Ensure a <head> tag exists, and insert <meta charset="utf-8"> into it
head = tree.find(".//head")
if head is None:
# If no head exists, create one and put it at the start of <html>
html_root = tree.getroot()
head = etree.Element("head")
html_root.insert(0, head)
# Check if a charset meta already exists to avoid duplicates
existing_meta = head.xpath(".//meta[@charset]")
if not existing_meta:
meta = etree.Element("meta")
meta.set("charset", "utf-8")
head.insert(0, meta) # Place it at the very top of head for parser efficiency
# 2. Your existing classification logic for <strong> elements
strong_elements = tree.xpath("//strong")
for el in strong_elements:
text = "".join(el.itertext())
# (Classification function logic goes here...)
el.set("data-text-type", "classified_type")
# 3. Save output
tree.write(output_path, encoding="utf-8", method="html")
print(f"Successfully processed and saved to: {output_path}")
UP: make a seperate function for <meta charset="utf-8">
Response: Here is the separate function to handle injecting the <meta charset="utf-8"> tag safely into your document's <head> section, along with how it integrates into your main processing script:
The Python Implementation
from lxml import etree
def ensure_utf8_meta(tree):
"""Ensures the HTML tree has a <meta charset="utf-8"> tag inside its <head>."""
html_root = tree.getroot()
if html_root is None:
return
# 1. Find or create the <head> element
head = html_root.find(".//head")
if head is None:
head = etree.Element("head")
# Insert <head> as the very first child of <html>
html_root.insert(0, head)
# 2. Check if a charset meta tag already exists
existing_meta = head.xpath(".//meta[@charset]")
if not existing_meta:
meta = etree.Element("meta")
meta.set("charset", "utf-8")
# Place it at the top of <head> for fast parser pickup
head.insert(0, meta)
def classify_strong_text(text):
text = text.strip()
if not text:
return "other"
letters = [c for c in text if c.isalpha()]
if not letters:
return "other"
if all(c.isupper() for c in letters):
return "type_1_article_title"
if letters[0].isupper() and any(c.islower() for c in letters):
return "type_2_section_header"
return "type_3_inline_bold"
def process_html_file(input_path, output_path):
parser = etree.HTMLParser(encoding="utf-8")
tree = etree.parse(input_path, parser)
# Ensure UTF-8 meta tag is declared using our separate function
ensure_utf8_meta(tree)
# Process <strong> elements
strong_elements = tree.xpath("//strong")
for el in strong_elements:
text = "".join(el.itertext())
classification = classify_strong_text(text)
el.set("data-text-type", classification)
# Save the modified tree
tree.write(output_path, encoding="utf-8", method="html")
print(f"Successfully processed and saved to: {output_path}")
UP: Let's fix anomaly of article title position. In normal case article title stays as a first child in a paragraph: p>strong[data-text-type="type_1_article_title"]. But in rare cases it goes inline with paragraph text, i.e. p>TextContent strong[data-text-type="type_1_article_title"]. Actually this is an abnormal positioning, which should be corrected to the normal case without TextContent before strong[data-text-type="type_1_article_title"]
Response: To correct the positioning anomaly where an article title (type_1_article_title) is trapped inline after regular text inside a paragraph, we can add a restructuring step right after the classification loop.
When this anomaly occurs, the script will split the paragraph: it will carve out the preceding text into its own separate paragraph, leaving the strong title as the sole (or leading) content of its own dedicated paragraph block.
Here is how you can implement this restructuring logic cleanly in Python using lxml:
Updated Python Processing Script
from lxml import etree
def ensure_utf8_meta(tree):
"""Ensures the HTML tree has a <meta charset="utf-8"> tag inside its <head>."""
html_root = tree.getroot()
if html_root is None:
return
head = html_root.find(".//head")
if head is None:
head = etree.Element("head")
html_root.insert(0, head)
existing_meta = head.xpath(".//meta[@charset]")
if not existing_meta:
meta = etree.Element("meta")
meta.set("charset", "utf-8")
head.insert(0, meta)
def classify_strong_text(text):
text = text.strip()
if not text:
return "other"
letters = [c for c in text if c.isalpha()]
if not letters:
return "other"
if all(c.isupper() for c in letters):
return "type_1_article_title"
if letters[0].isupper() and any(c.islower() for c in letters):
return "type_2_section_header"
return "type_3_inline_bold"
def fix_inline_article_titles(tree):
"""
Finds paragraphs where a type_1_article_title is preceded by text content,
and splits them so the title becomes its own paragraph at the top.
"""
# Find all type_1 strong elements
type_1_elements = tree.xpath("//strong[@data-text-type='type_1_article_title']")
for strong_el in type_1_elements:
parent_p = strong_el.getparent()
# Only process if it's inside a <p> tag
if parent_p is not None and parent_p.tag == 'p':
# Check if there is actual text content or preceding nodes *before* this <strong> tag inside the <p>
# We look at preceding siblings or direct text of the parent paragraph before the element.
preceding_text = strong_el.text_tail or "" # check tail? No, tail is after.
# A cleaner way in lxml to see if it's the first child:
if parent_p[0] != strong_el:
# Anomaly detected: There are nodes/text preceding the title inside the same paragraph.
# We need to split the paragraph.
# 1. Create a new <p> element for the title
new_p = etree.Element('p')
# Copy attributes from original paragraph if any
for k, v in parent_p.attrib.items():
new_p.set(k, v)
# 2. Move the strong element (and any nodes following it in that messy paragraph) into the new <p>
# Gather all siblings from strong_el onwards
siblings_to_move = []
is_target = False
for child in parent_p:
if child == strong_el:
is_target = True
if is_target:
siblings_to_move.append(child)
# Append them to the new paragraph
for child in siblings_to_move:
parent_p.remove(child)
new_p.append(child)
# 3. Insert the new title paragraph right *before* the original paragraph in the DOM tree
parent_p.addprevious(new_p)
# 4. If the original paragraph is now completely empty, clean it up
if not len(parent_p) and not (parent_p.text and parent_p.text.strip()):
parent_p.getparent().remove(parent_p)
def process_html_file(input_path, output_path):
parser = etree.HTMLParser(encoding="utf-8")
tree = etree.parse(input_path, parser)
# 1. Inject UTF-8 meta tag
ensure_utf8_meta(tree)
# 2. Classify all <strong> elements
strong_elements = tree.xpath("//strong")
for el in strong_elements:
text = "".join(el.itertext())
classification = classify_strong_text(text)
el.set("data-text-type", classification)
# 3. Fix anomalous inline article titles by restructuring the DOM
fix_inline_article_titles(tree)
# 4. Save the corrected tree
tree.write(output_path, encoding="utf-8", method="html")
print(f"Successfully processed and saved to: {output_path}")
How this restructuring logic works:
- Target Detection: It targets every strong element that has been classified as type_1_article_title.
- Position Check: It checks if the strong element is the very first child (parent_p[0]) of its parent paragraph.
- DOM Splitting: If it is not the first child (meaning text or elements appear before it), the script dynamically creates a brand new paragraph (new_p), safely detaches the title (and any trailing elements attached to it) from the old paragraph, moves them into the new paragraph, and inserts that new paragraph right above the old one.
UP: I don't think we should insert a new paragraph before initial one, but rather after
Response: To change the restructuring flow so that the newly split article title paragraph is inserted after the original paragraph instead of before it, we simply adjust how the new element is injected into the DOM tree.
Here is the updated fix_inline_article_titles function with that adjustment:
def fix_inline_article_titles(tree):
"""
Finds paragraphs where a type_1_article_title is preceded by text content,
and splits them so the title becomes its own paragraph placed *after* the text content.
"""
# Find all type_1 strong elements
type_1_elements = tree.xpath("//strong[@data-text-type='type_1_article_title']")
for strong_el in type_1_elements:
parent_p = strong_el.getparent()
# Only process if it's inside a <p> tag
if parent_p is not None and parent_p.tag == 'p':
# Check if it is NOT the first child (meaning there is text/nodes before it)
if parent_p[0] != strong_el:
# 1. Create a new <p> element for the title
new_p = etree.Element('p')
for k, v in parent_p.attrib.items():
new_p.set(k, v)
# 2. Gather the strong element and any subsequent nodes to move them
siblings_to_move = []
is_target = False
for child in parent_p:
if child == strong_el:
is_target = True
if is_target:
siblings_to_move.append(child)
# Move them out of the old paragraph and into the new one
for child in siblings_to_move:
parent_p.remove(child)
new_p.append(child)
# 3. Insert the new title paragraph right *after* the original paragraph in the DOM tree
parent_p.addnext(new_p)
# 4. If the original paragraph is now completely empty, clean it up
if not len(parent_p) and not (parent_p.text and parent_p.text.strip()):
parent_p.getparent().remove(parent_p)
What changed:
- parent_p.addnext(new_p): Replaced addprevious with addnext. This ensures that when an inline article title is found trapped behind preceding text, the text stays in the original paragraph and the title is cleanly split off into its own new paragraph directly following it.
UP: can u compose me a XPath to detect an abnormal case in Firefox html search
Response: To detect this specific anomalous case directly inside the Firefox DevTools Inspector search bar (Ctrl + F), you can use a native XPath 1.0 query.
Paste this exact XPath expression into the search box:
//p[strong][1]/strong[not(parent::p/child::*[1] = .)]
(Alternatively, you can target it from the strong tag's perspective: //strong[@data-text-type='type_1_article_title'][preceding-sibling::node()])
How this Firefox Inspector query works:
- //p[strong]: Finds all paragraphs that contain a <strong> tag.
- [1]: Confines the evaluation logic to clean up indexing.
- /strong[not(parent::p/child::*[1] = .)]: Selects the <strong> elements inside those paragraphs only if the strong element is not the very first child node of its parent paragraph (.).
If an article title is cleanly positioned at the beginning, Firefox will find 0 matches. If it is trapped inline after random text or nodes, Firefox will immediately highlight the anomalous title element in your DOM tree!
UP: didn't work
Response: That query likely failed in Firefox DevTools because Firefox's built-in Inspector search bar gets confused by complex node-identity evaluations (= .) on structural axes.
Instead, a much simpler, robust way to find an element that has preceding sibling text or nodes inside its parent paragraph is to look for strong elements that have any preceding sibling node at all:
//p/strong[preceding-sibling::node()]
Why this works:
- //p/strong: Targets any strong tag directly inside a paragraph.
- [preceding-sibling::node()]: Filters it down so it only matches if there is anything (text, spaces, or other tags) sitting right before it inside that same paragraph. If the strong tag is the absolute first child of the paragraph, this evaluates to false and skips it.
UP: confine search results only to strong[data-text-type="type_1_article_title"]
Response: To confine your search results strictly to Type 1 article titles that are incorrectly placed inline (meaning they are preceded by text or nodes inside the same paragraph), use this XPath expression in the Firefox Inspector:
//p/strong[preceding-sibling::node()][not(@data-text-type) or @data-text-type='type_1_article_title']
Or, if your HTML elements have already been fully classified by your Python script and you are certain the attribute is present on all of them, you can use this cleaner version:
//p/strong[@data-text-type='type_1_article_title' and preceding-sibling::node()]
Why this works:
- //p/strong: Targets strong tags inside paragraphs.
- [@data-text-type='type_1_article_title']: Strictly filters for your designated Type 1 article titles.
- [preceding-sibling::node()]: Ensures it only selects them if they are trapped after other text or nodes, successfully bypassing normally formatted article titles.
UP: I have place fix_inline_article_titles function in a seperate inline_title_anomaly_fix.py file along with strong_text_classifier.py. I need to use functions inside the second file. Will Python see them
Response: Yes, Python can see them, but only if you explicitly import them.
Python modules do not automatically know about each other just because they are saved in the same folder. If inline_title_anomaly_fix.py needs to use functions (like classify_strong_text or ensure_utf8_meta) from strong_text_classifier.py, you must import them at the top of your file.
Here is how you set it up:
1. Structure your files correctly
Make sure both files are in the same directory:
your_project_folder/
│
├── strong_text_classifier.py
└── inline_title_anomaly_fix.py
2. Add the import statement
Inside your inline_title_anomaly_fix.py file, add an import statement at the very top referencing the other module by its filename (without the .py extension):
# inline_title_anomaly_fix.py
from lxml import etree
# Import the functions from your other file
from strong_text_classifier import classify_strong_text, ensure_utf8_meta
def fix_inline_article_titles(tree):
# Now you can use classify_strong_text() or ensure_utf8_meta() right here!
type_1_elements = tree.xpath("//strong[@data-text-type='type_1_article_title']")
for strong_el in type_1_elements:
parent_p = strong_el.getparent()
if parent_p is not None and parent_p.tag == 'p':
if parent_p[0] != strong_el:
new_p = etree.Element('p')
for k, v in parent_p.attrib.items():
new_p.set(k, v)
siblings_to_move = []
is_target = False
for child in parent_p:
if child == strong_el:
is_target = True
if is_target:
siblings_to_move.append(child)
for child in siblings_to_move:
parent_p.remove(child)
new_p.append(child)
parent_p.addnext(new_p)
if not len(parent_p) and not (parent_p.text and parent_p.text.strip()):
parent_p.getparent().remove(parent_p)
As long as both .py files share the same directory, Python will successfully find, load, and execute functions across both files via that from ... import ... statement.
UP: classify_strong_text.py file saved without underscore chars
Response: If your file was saved without underscore characters as strongtextclassifier.py, Python will raise a ModuleNotFoundError if you try to import it using the original name because filenames are case-sensitive and literal.
To fix this, you have two options:
Option 1: Rename the file (Recommended)
Rename strongtextclassifier.py on your computer to include the underscores: strong_text_classifier.py. This matches standard Python naming conventions (PEP 8) and keeps your import statements clean.
Option 2: Update your import statement to match the filename
If you prefer to keep the file named strongtextclassifier.py, just update your from ... import ... statement in inline_title_anomaly_fix.py to match the exact filename (without the .py extension):
# inline_title_anomaly_fix.py
from lxml import etree
# Match the exact name of your file on disk (no underscores)
from strongtextclassifier import classify_strong_text, ensure_utf8_meta
# Rest of your fix_inline_article_titles function goes here...
UP: I'm in VisualCode. How to import another py file from a different directory
Response: If your files are located in different directories (folders), Python cannot find them automatically by default, even if they are open in VS Code workspace tabs.
How you import them depends on how your folders are structured. Here are the two most common scenarios:
Scenario A: Sibling Directories (Folders next to each other)
If your folder structure looks like this:
my_project/
├── classifier_folder/
│ └── strongtextclassifier.py
└── fixer_folder/
└── inline_title_anomaly_fix.py
To pull the function from a neighboring folder into inline_title_anomaly_fix.py, you need to temporarily tell Python's system path where to look by adding a few lines at the very top of your file before doing the import:
import sys
import os
# 1. Dynamically add the parent/sibling directory to Python's search path
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
target_folder_path = os.path.join(parent_dir, 'classifier_folder')
sys.path.append(target_folder_path)
# 2. Now Python can see it and import normally!
from strongtextclassifier import classify_strong_text, ensure_utf8_meta
Scenario B: Subdirectory (One folder is inside the other)
If your folder structure looks like this:
my_project/
├── main_runner.py
└── utils/
└── strongtextclassifier.py
If you are writing code in main_runner.py and want to import a function from the utils subfolder, VS Code makes this very easy using dot-notation:
# Import directly using the folder name as a namespace package
from utils.strongtextclassifier import classify_strong_text, ensure_utf8_meta
(Tip: In VS Code, if you start typing from utils., IntelliSense auto-completion will pop up and help you select the exact file and function automatically).
UP: My folders have whitespaces in their names
Response: Having whitespaces in your folder names (e.g., My Project or HTML Files) is completely fine for Python imports as long as you handle the paths correctly in your code or structure them as a package.
Here is how you handle imports when your directories contain spaces:
1. If using sys.path.append (For separate/sibling folders with spaces)
Python's sys.path handles strings with spaces natively. Just write the folder name with its whitespace as it appears on your computer:
import sys
import os
# Get the directory of the current file
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
# Safely point to the folder with whitespace in its name
classifier_folder_path = os.path.join(parent_dir, 'My Classifier Folder')
sys.path.append(classifier_folder_path)
# Import normally
from strongtextclassifier import classify_strong_text, ensure_utf8_meta
2. If using subdirectories (Dot notation)
If one folder is inside another and the parent folder contains a whitespace, standard Python import dot-notation will fail because Python treats dots as directory separators and spaces as syntax errors.
The fix: Use standard dynamic importing via importlib instead:
import importlib.util
import os
# 1. Build the absolute path to the file inside the folder with spaces
current_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(current_dir, 'My Folder With Spaces', 'strongtextclassifier.py')
# 2. Load the module dynamically
spec = importlib.util.spec_from_file_location("strongtextclassifier", file_path)
strong_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(strong_module)
# 3. Use your functions directly from the loaded module
classify_strong_text = strong_module.classify_strong_text
ensure_utf8_meta = strong_module.ensure_utf8_meta
Pro-tip: If possible, avoiding whitespaces in folder names for coding projects saves you from path resolution headaches down the road, but the dynamic loading snippet above will bypass it seamlessly if you need to keep them!
UP: my_project/ ├── Parsermain_runner.py └── utils/ └── strongtextclassifier.py
Response: Since your folder structure is clean and has no whitespace in the directory names, you can import functions from your utils subfolder directly using standard Python dot notation.
Here is how you write the import statement at the top of your Parsermain_runner.py file:
# Parsermain_runner.py
from utils.strongtextclassifier import classify_strong_text, ensure_utf8_meta
# Now you can use the functions directly!
# Example:
# tree = ...
# ensure_utf8_meta(tree)
Why this works:
- utils/: Because utils is a subfolder right next to your runner script, Python recognizes it as a package directory.
- strongtextclassifier: The name of your file (without .py).
- VS Code Tip: In VS Code, if you type from utils., IntelliSense will automatically pop up a dropdown list showing strongtextclassifier, making it easy to autocomplete.
UP: how in command line draw a dir tree
Response: To draw a directory tree right from your command line (or the built-in terminal inside VS Code), use the native command depending on your operating system:
1. Windows (Command Prompt / PowerShell)
Windows has a built-in command called tree. To display both folders and files in a clean visual layout, run:
tree /F /A
- /F: Displays the names of the files within each folder.
- /A: Uses standard text characters instead of extended ASCII symbols (preventing weird character glitches in some terminals).
2. macOS / Linux (Terminal)
Mac and Linux systems often use the tree command as well. If it's not installed by default, you can easily install it via Homebrew (brew install tree on Mac) or apt (sudo apt install tree on Ubuntu/Debian).
To run it, simply type:
tree
💡 Quick Python Alternative (Works everywhere)
If your terminal doesn't have tree installed or you want a quick platform-independent script, you can run this one-liner directly in your VS Code terminal using Python to print out your project structure:
python -c "import os; print('\n'.format(*(p[0].replace(os.sep,'/') + '/' + f for p in os.walk('.') for f in p[2])))"
(Or simply use the built-in VS Code File Explorer pane on the left side of your window to visually browse your folders at a glance!)
Is there a specific folder layout you are trying to verify for your MediaWiki migration scripts?
Sources
- Display the Linux directory tree with the tree command
UP: I have this structure: root└── 4. Processor [parsing] ├── 1. strong text classifier │ ├── input │ │ └── 2-50 [V5][styles]_[ImgCapt]_clean.html │ ├── output │ │ ├── 2-13 v2 [vol5][imgC]_[ImgCapt]_transformed.html │ │ └── 2-50 v3 [styles][ImgCapt][cleanHtml][strongClassified].html │ └── src │ ├── strong_text_classifier.py │ └── strong_text_classifier_v2.py ├── 2. inline_title_anomaly_fixer │ └── inline_title_anomaly fix.py └── MainParser └── Parse.py
Response: Based on your directory layout, your runner script (Parse.py) needs to pull in functions from a sibling folder (1. strong text classifier) and a different sibling folder (2. inline_title_anomaly_fixer), both of which contain spaces and numbers in their folder names.
To allow Python to cross into folders with spaces safely without breaking your file structure, use dynamic path insertion and importing inside your Parse.py script.
How to write Parse.py to import everything:
import os
import sys
import importlib.util
# 1. Get the absolute path of the MainParser directory
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(current_dir) # Goes up to root workspace
# 2. Define exact paths to your script folders (handling spaces natively)
classifier_folder = os.path.join(root_dir, '4. Processor [parsing]', '1. strong text classifier', 'src')
fixer_folder = os.path.join(root_dir, '4. Processor [parsing]', '2. inline_title_anomaly_fixer')
# 3. Function to dynamically load a python module from a specific file path
def load_module_from_path(module_name, file_path):
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
# 4. Load your classifier and fixer modules
classifier_path = os.path.join(classifier_folder, 'strong_text_classifier.py')
fixer_path = os.path.join(fixer_folder, 'inline_title_anomaly fix.py')
classifier_mod = load_module_from_path("strong_text_classifier", classifier_path)
fixer_mod = load_module_from_path("inline_title_anomaly_fix", fixer_path)
# 5. Extract your functions ready for use!
classify_strong_text = classifier_mod.classify_strong_text
ensure_utf8_meta = classifier_mod.ensure_utf8_meta
fix_inline_article_titles = fixer_mod.fix_inline_article_titles # Assuming function name inside that file
# Example execution flow:
def process_full_pipeline(input_file, output_file):
from lxml import etree
parser = etree.HTMLParser(encoding="utf-8")
tree = etree.parse(input_file, parser)
# Run steps
ensure_utf8_meta(tree)
for el in tree.xpath("//strong"):
text = "".join(el.itertext())
el.set("data-text-type", classify_strong_text(text))
fix_inline_article_titles(tree)
tree.write(output_file, encoding="utf-8", method="html")
print(f"Pipeline complete! Saved to {output_file}")
Why this approach fits your layout:
- Handles Spaces & Numbers: Python regular import statements choke on spaces and leading numbers in folder names (like 1. strong text classifier). Using importlib.util bypasses this entirely by targeting the exact file path safely.
- Centralized Runner: Keeps Parse.py clean as the master orchestrator while leaving your modular code safely stored inside their respective subfolders.
UP: how to access Parse.py from strong_text_classifier_v2.py
Response: To allow a lower-level file like strong_text_classifier_v2.py to access code from Parse.py (which lives higher up in the directory tree), you need to temporarily append the parent directory (or the root workspace) to Python's sys.path.
This allows strong_text_classifier_v2.py to "look upward" and import modules from MainParser.
The Python Solution
Add this snippet to the very top of your strong_text_classifier_v2.py file before trying to import anything from Parse.py:
import os
import sys
# 1. Dynamically find the path to the root or MainParser folder
current_dir = os.path.dirname(os.path.abspath(__file__))
# Depending on how deep you need to go up:
# Go up from src -> 1. strong text classifier -> 4. Processor [parsing] -> root
root_processor_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
# Path to the MainParser directory
main_parser_dir = os.path.join(root_processor_dir, 'MainParser')
# 2. Add it to Python's system path so it can see Parse.py
if main_parser_dir not in sys.path:
sys.path.append(main_parser_dir)
# 3. Now you can import functions or variables from Parse
import Parse
# Example usage:
# Parse.some_shared_function()
💡 Architectural Note
In clean software design, lower-level utility modules (like classifiers) usually shouldn't depend on upper-level runner scripts (Parse.py), because it creates a circular dependency or tightly couples your core logic to your runner script.
If strong_text_classifier_v2.py just needs shared helper functions, it's often better to move those shared helpers into a separate utility file or keep Parse.py as the only script that imports downward into the processors. However, if you strictly need access to Parse.py, the sys.path.append method above will successfully bridge the gap!
UP: how to type new line here in the prompt line
Response: To insert a new line in the chat input box without accidentally sending your message, use the standard shortcut based on your operating system:
- Windows / Linux: Press Shift + Enter
- Mac: Press Shift + Return
(Pressing Enter or Return alone will send the message, while holding Shift drops you down to a new line.)
UP: I have updated my dir tree based on your recommendation:
root ── 4. Processor [parsing] ├── 1. strong text classifier │ ├── input │ │ └── 2-50 [V5][styles]_[ImgCapt]_clean.html │ ├── output │ │ ├── 2-13 v2 [vol5][imgC]_[ImgCapt]_transformed.html │ │ └── 2-50 v3 [styles][ImgCapt][cleanHtml][strongClassified].html │ └── src │ ├── strong_text_classifier.py │ └── strong_text_classifier_v2.py ├── 2. inline_title_anomaly_fixer │ └── inline_title_anomaly fix.py ├── Common │ └── common_lib.py └── MainParser └── Parse.py I need to access common_lib.py from strong_text_classifier_v2.py. is there an easy way?
Response: Yes, there is a very clean and easy way to access common_lib.py from strong_text_classifier_v2.py!
Since you created a dedicated Common folder, you can use Python's sys.path to point upward to it. Because folder names with spaces and numbers like 4. Processor [parsing] can sometimes cause issues with relative dot-notation, using an absolute path lookup relative to your current file is the most bulletproof approach.
Add these lines to the very top of your strong_text_classifier_v2.py file:
import os
import sys
# 1. Dynamically calculate the path to the 'Common' folder
current_dir = os.path.dirname(os.path.abspath(__file__))
# Go up 3 levels: src -> 1. strong text classifier -> 4. Processor [parsing] -> then into Common
common_dir = os.path.abspath(os.path.join(current_dir, '..', '..', '..', 'Common'))
# 2. Add it to Python's system path if it isn't there already
if common_dir not in sys.path:
sys.path.append(common_dir)
# 3. Import your library/functions normally!
import common_lib
# Example usage:
# common_lib.your_shared_function()
Why this works seamlessly:
- os.path.abspath(__file__) gets the exact location of your strong_text_classifier_v2.py file no matter where you run the script from.
- '..' steps backward out of src, then 1. strong text classifier, then 4. Processor [parsing], and steps into Common.
- No __init__.py required: Because we are appending the folder directly to sys.path, Python instantly recognizes common_lib as a module it can import without needing empty package init files.
UP: put this to a seperate function: # Process <strong> elements strong_elements = tree.xpath("//strong") for el in strong_elements: text = "".join(el.itertext()) classification = classify_strong_text(text) el.set("data-text-type", classification)
Response: Here is that block of code neatly wrapped into its own standalone function. You can place this inside your strong_text_classifier_v2.py file or drop it into common_lib.py if you plan to share it across multiple scripts:
def classify_and_tag_strong_elements(tree, classify_func):
"""
Finds all <strong> elements in the HTML tree, runs them through
the classification function, and sets the 'data-text-type' attribute.
"""
strong_elements = tree.xpath("//strong")
for el in strong_elements:
text = "".join(el.itertext())
classification = classify_func(text)
el.set("data-text-type", classification)
return tree
How to use it in your code:
# Example call inside your processing pipeline:
classify_and_tag_strong_elements(tree, classify_strong_text)
UP: In VCode project/settings.json { "python.analysis.extraPaths": [ "./4. Processor [parsing]/strong text classifier/src", "./4. Processor [parsing]/1. strong text classifier/src", "./4. Processor [parsing]/Common", "./4. Processor [parsing]/2. inline_title_anomaly_fixer" ] } were added, but no module found exception is thrown at from strong_text_classifier_v2 import classify_strong_text
Response: The reason you are still getting a ModuleNotFoundError is because python.analysis.extraPaths in VS Code only helps the Pylance linter see the code for syntax highlighting and autocomplete. It does not actually inject those directories into Python's active runtime sys.path when you execute the script.
To fix this so Python can successfully resolve and execute the import at runtime, you have two options:
Option 1: Use absolute workspace paths in VS Code variables
Update your .vscode/settings.json file to use ${workspaceFolder}, which correctly resolves absolute paths for both the editor and Python's language server:
{
"python.analysis.extraPaths": [
"${workspaceFolder}/4. Processor [parsing]/1. strong text classifier/src",
"${workspaceFolder}/4. Processor [parsing]/Common",
"${workspaceFolder}/4. Processor [parsing]/2. inline_title_anomaly_fixer"
]
}
Option 2: Add runtime path resolution inside your script (Recommended)
Since folder names containing spaces and leading numbers (1. strong text classifier) can trick standard Python execution paths, the most reliable way to make Python see it at runtime is to explicitly append the absolute folder path to sys.path right inside your Python file before importing.
Add this snippet to the top of the script where you are running the import:
import os
import sys
# Dynamically point to the 'src' folder containing strong_text_classifier_v2
current_dir = os.path.dirname(os.path.abspath(__file__))
classifier_src_dir = os.path.abspath(os.path.join(current_dir, '..', '1. strong text classifier', 'src'))
if classifier_src_dir not in sys.path:
sys.path.append(classifier_src_dir)
# Now Python will successfully find and load it at runtime
from strong_text_classifier_v2 import classify_strong_text
(Note: After changing settings.json, you may also need to restart the VS Code language server by pressing Ctrl + Shift + P and typing Python: Clear Cache and Reload Window.)
UP: Option 2. adds only one strong_text_classifier_v2 path. What about others
Response: You can easily extend Option 2 to register all of your project folders into Python's sys.path in one centralized place at the very top of your master runner (Parse.py).
By defining your project root once and mapping all subfolders, every module will instantly become visible to Python at runtime without needing manual path adjustments in every single file.
Centralized Runtime Path Register (for Parse.py)
Add this block to the very top of your Parse.py script:
import os
import sys
# 1. Locate the root directory (where '4. Processor [parsing]' lives)
# Adjust the '..' count based on how deep Parse.py is nested inside MainParser
current_dir = os.path.dirname(os.path.abspath(__file__))
root_processor_dir = os.path.abspath(os.path.join(current_dir, '..', '..'))
# 2. Define all your custom module paths using the root base
folder_paths = [
os.path.join(root_processor_dir, '4. Processor [parsing]', '1. strong text classifier', 'src'),
os.path.join(root_processor_dir, '4. Processor [parsing]', '2. inline_title_anomaly_fixer'),
os.path.join(root_processor_dir, '4. Processor [parsing]', 'Common')
]
# 3. Automatically append all paths to sys.path if they aren't already there
for path in folder_paths:
if path not in sys.path:
sys.path.append(path)
# 4. Now Python can seamlessly import from ANY of your folders!
from strong_text_classifier_v2 import classify_strong_text
# import inline_title_anomaly_fix # (if named that way)
# import common_lib
Why this is the best approach for your layout:
- One-Stop Shop: You only need to write this path-mapping logic once inside your entry point (Parse.py).
- Bypasses Whitespace Issues: Using os.path.join handles spaces and numeric prefixes in folder names natively across Windows, macOS, and Linux.
UP: will the changes to sys_path in tact after programm execution
Response: No, the changes to sys.path do not persist after program execution.
sys.path is just a standard Python list stored purely in your computer's RAM while your script is actively running.
What happens behind the scenes:
- During Execution: When you run Parse.py, Python loads sys.path, appends your custom paths to that active list in memory, and uses them to find your modules.
- After Extermination/Completion: The moment your script finishes running (or crashes), the Python process closes, and that instance of RAM is completely wiped.
- Next Run: The next time you execute Parse.py, Python starts with a fresh, default sys.path, which is why your script explicitly re-runs the sys.path.append() code at the top every time it boots up.
It leaves your global system environment and your project files completely untouched and clean!
UP: can u shape this path adding in a seperate function?
Response: Here is the sys.path registration logic wrapped into a clean, reusable function.
You can place this function right inside your Parse.py runner script (or even save it inside your Common/common_lib.py file to call it from anywhere):
import os
import sys
def setup_project_paths():
"""
Dynamically registers all project subdirectories with spaces and
numeric prefixes into Python's runtime sys.path.
"""
# Locate the root '4. Processor [parsing]' directory based on Parse.py's location
current_dir = os.path.dirname(os.path.abspath(__file__))
root_processor_dir = os.path.abspath(os.path.join(current_dir, '..', '..'))
# Define all custom module directories
folder_paths = [
os.path.join(root_processor_dir, '4. Processor [parsing]', '1. strong text classifier', 'src'),
os.path.join(root_processor_dir, '4. Processor [parsing]', '2. inline_title_anomaly_fixer'),
os.path.join(root_processor_dir, '4. Processor [parsing]', 'Common')
]
# Append paths to sys.path if they are missing
for path in folder_paths:
if path not in sys.path and os.path.exists(path):
sys.path.append(path)
How to use it in Parse.py:
At the very top of your main execution script, you just call the function once before doing any imports:
# Parse.py
# 1. Setup the runtime paths first
from setup_paths_module import setup_project_paths # (or define it right above)
setup_project_paths()
# 2. Now import whatever modules you need smoothly!
from strong_text_classifier_v2 import classify_strong_text
# from inline_title_anomaly_fix import fix_inline_article_titles
# import common_lib
UP: MainParser and Common dirs are on the same level. Can Parse.py access common_lib_py?
Response: Yes, absolutely! Because MainParser and Common are sibling directories sitting right next to each other inside 4. Processor [parsing], Parse.py can easily access common_lib.py.
How to adjust the path for Parse.py
Since Parse.py is nested one level down inside MainParser, it just needs to step up one level to reach 4. Processor [parsing], and then drop into Common.
Here is how you update your setup_project_paths() function in Parse.py to include it:
import os
import sys
def setup_project_paths():
"""
Dynamically registers all project subdirectories into Python's runtime sys.path.
"""
# 1. Get the directory where Parse.py lives (MainParser)
current_dir = os.path.dirname(os.path.abspath(__file__))
# 2. Step up ONE level to reach '4. Processor [parsing]'
processor_root = os.path.dirname(current_dir)
# 3. Define all paths relative to the processor root
folder_paths = [
os.path.join(processor_root, '1. strong text classifier', 'src'),
os.path.join(processor_root, '2. inline_title_anomaly_fixer'),
os.path.join(processor_root, 'Common') # <--- Common is right here!
]
# 4. Append them to sys.path
for path in folder_paths:
if path not in sys.path and os.path.exists(path):
sys.path.append(path)
Once registered, you can import it directly:
# At the top of Parse.py
setup_project_paths()
import common_lib
# Now you can use functions from common_lib!
UP: I mean I have placed setup_project_paths inside common_lib.py and now want to access it from Parse.py
Response: To call setup_project_paths() from common_lib.py inside Parse.py, you just need a tiny bootstrap path addition for Parse.py to find the Common folder before it can import common_lib.
Since MainParser and Common are siblings, Parse.py can step up one level to reach Common, import the function, run it, and then unlock all your other folders!
Here is how you write Parse.py:
import os
import sys
# 1. Bootstrap: Temporarily add the 'Common' sibling directory so we can import common_lib
current_dir = os.path.dirname(os.path.abspath(__file__))
processor_root = os.path.dirname(current_dir)
common_dir = os.path.join(processor_root, 'Common')
if common_dir not in sys.path:
sys.path.append(common_dir)
# 2. Now import your setup function from common_lib and run it!
from common_lib import setup_project_paths
setup_project_paths()
# 3. All other project folders are now unlocked! Import anything you need:
from strong_text_classifier_v2 import classify_strong_text
# from inline_title_anomaly_fix import fix_inline_article_titles
What your common_lib.py should look like:
Make sure your function inside common_lib.py uses processor_root correctly (stepping up from Common to reach 4. Processor [parsing]):
# Common/common_lib.py
import os
import sys
def setup_project_paths():
"""
Dynamically registers all project subdirectories into Python's runtime sys.path.
"""
# Get the directory where common_lib.py lives (Common)
current_dir = os.path.dirname(os.path.abspath(__file__))
# Step up ONE level to reach '4. Processor [parsing]'
processor_root = os.path.dirname(current_dir)
# Define all target folders
folder_paths = [
os.path.join(processor_root, '1. strong text classifier', 'src'),
os.path.join(processor_root, '2. inline_title_anomaly_fixer'),
os.path.join(processor_root, 'Common')
]
# Append valid paths
for path in folder_paths:
if path not in sys.path and os.path.exists(path):
sys.path.append(path)
UP: 4. Processor [parsing] is not the root, it's a child of the root
Response: Ah, that clarifies the structure! Since 4. Processor [parsing] is a child of your actual workspace root, let's make sure the path-finding logic steps up the correct number of levels so it can find Common.
Here is how you can write setup_project_paths() inside common_lib.py to correctly locate the workspace root and find all your sibling folders:
1. Updated common_lib.py
# Common/common_lib.py
import os
import sys
def setup_project_paths():
"""
Dynamically registers all project subdirectories into Python's runtime sys.path.
"""
# Get the directory where common_lib.py lives (Common)
current_dir = os.path.dirname(os.path.abspath(__file__))
# Step up from Common -> 4. Processor [parsing] -> root workspace
processor_root = os.path.dirname(current_dir)
workspace_root = os.path.dirname(processor_root)
# Define all target folders relative to '4. Processor [parsing]'
folder_paths = [
os.path.join(processor_root, '1. strong text classifier', 'src'),
os.path.join(processor_root, '2. inline_title_anomaly_fixer'),
os.path.join(processor_root, 'Common')
]
# Append valid paths to sys.path
for path in folder_paths:
if path not in sys.path and os.path.exists(path):
sys.path.append(path)
2. How Parse.py uses it
Because MainParser and Common are both inside 4. Processor [parsing], Parse.py just needs to step up one level to reach Common for the initial bootstrap import:
# MainParser/Parse.py
import os
import sys
# 1. Bootstrap: Temporarily add the 'Common' sibling directory
current_dir = os.path.dirname(os.path.abspath(__file__))
processor_root = os.path.dirname(current_dir)
common_dir = os.path.join(processor_root, 'Common')
if common_dir not in sys.path:
sys.path.append(common_dir)
# 2. Import and run the path setup function from common_lib
from common_lib import setup_project_paths
setup_project_paths()
# 3. All folders are now unlocked—import your modules freely!
from strong_text_classifier_v2 import classify_strong_text
This keeps your imports clean and dynamically adapts to your workspace layout, regardless of spaces or numbering in the folder names!
UP: it's time to derive article titles and inject appropriate tags to the html tree. To do this I need to get all strong elements with type1 attribute, trim tailing punctuation and insert new title element before original strong element.
Response: To achieve this, you can target all elements tagged with your attribute (e.g., [@data-text-type='Article Title']), clean up their trailing punctuation using Python's string.punctuation, and then use lxml's element parent methods to insert a brand-new title element (like an <h2> or <p> tag) right before the original <strong> element.
Here is a robust function to handle this transformation:
The Transformation Function
import string
from lxml import etree
def extract_and_inject_article_titles(tree, tag_name="h2", attr_selector="[@data-text-type='Article Title']"):
"""
Finds strong elements matching the selector, trims trailing punctuation,
creates a new title element, and inserts it right before the original node.
"""
# 1. Find all strong elements with the specified type attribute
xpath_query = f"//strong{attr_selector}"
target_elements = tree.xpath(xpath_query)
for el in target_elements:
# Get full text content inside the strong tag
raw_text = "".join(el.itertext()).strip()
if not raw_text:
continue
# 2. Trim trailing punctuation (e.g., colons, periods, dashes)
clean_title_text = raw_text.rstrip(string.punctuation).strip()
# 3. Create a new title element (e.g., <h2>)
new_title_el = etree.Element(tag_name)
new_title_el.text = clean_title_text
# Optional: Copy classes or assign attributes to the new element if needed
new_title_el.set("class", "extracted-article-title")
# 4. Insert the new element right before the parent of <strong>
# (or directly before <strong> depending on your document flow layout)
parent = el.getparent()
if parent is not None:
# Find the exact index of the current element to insert right before it
idx = parent.index(el)
parent.insert(idx, new_title_el)
# Optional: If you want to clean or alter the original <strong> tag
# after extracting it, you can modify it or clear its text here.
return tree
How it operates in your pipeline:
- XPath Selection: It queries elements carrying your classification tag (e.g., data-text-type="Article Title").
- Punctuation Stripping: raw_text.rstrip(string.punctuation) safely strips off trailing artifacts like colons (:) or periods (.) without altering core words.
- DOM Insertion (parent.insert): It queries the parent node, discovers the precise index of the target element, and splices the new header element immediately ahead of it.
Sources
- How do I strip all leading and trailing punctuation in Python?
- Python: lxml insert element before and after given element - Linux Info
UP: i think selector should be data-text-type="type_1_article_title"
Response: That makes total sense! Updating the selector to use data-text-type="type_1_article_title" keeps your classification values clean and standardized.
Here is the updated function reflecting your exact attribute selector:
import string
from lxml import etree
def extract_and_inject_article_titles(tree, tag_name="h2", attr_selector="[@data-text-type='type_1_article_title']"):
"""
Finds strong elements matching the type_1_article_title selector,
trims trailing punctuation, creates a new title element, and inserts
it right before the original <strong> node.
"""
# 1. Target strong elements with your specific type_1 attribute
xpath_query = f"//strong{attr_selector}"
target_elements = tree.xpath(xpath_query)
for el in target_elements:
# Get full text content inside the strong tag
raw_text = "".join(el.itertext()).strip()
if not raw_text:
continue
# 2. Trim trailing punctuation (e.g., colons, periods, dashes)
clean_title_text = raw_text.rstrip(string.punctuation).strip()
# 3. Create a new title element (e.g., <h2>)
new_title_el = etree.Element(tag_name)
new_title_el.text = clean_title_text
new_title_el.set("class", "extracted-article-title")
# 4. Insert the new element right before the original <strong> tag
parent = el.getparent()
if parent is not None:
idx = parent.index(el)
parent.insert(idx, new_title_el)
return tree
You can place this helper function inside one of your utility modules (or common_lib.py) and call it directly from your main execution pipeline right after your classification step runs.
Would you like to drop this into inline_title_anomaly fix.py or keep it alongside your classifier logic?
UP: strong[@data-text-type='type_1_article_title'] usually wropped in its own p element which stays directly under the body element: body>p>strong[@data-text-type='type_1_article_title']. It turns out <title> element should not be inserted directly before strong[@data-text-type='type_1_article_title'] but before its p parent: body>title is a preceding sibling of p>strong[@data-text-type='type_1_article_title'].
Response: That is a crucial structural detail! Since your target <strong data-text-type="type_1_article_title"> elements are wrapped individually inside their own <p> tags directly under the <body>, inserting the new title element before the <strong> would leave an empty or unwanted <p> wrapper behind.
Instead, we want to target the parent <p> element and insert the new title element directly before that <p> node (making it a sibling to the <p> under <body>).
Here is the updated logic to handle that exact DOM hierarchy:
import string
from lxml import etree
def extract_and_inject_article_titles(tree, tag_name="h2", attr_selector="[@data-text-type='type_1_article_title']"):
"""
Finds strong elements matching the selector, gets their parent <p> tag,
trims trailing punctuation, creates a new title element, and inserts
it right BEFORE the parent <p> element under the body.
"""
# 1. Target the specific strong elements
xpath_query = f"//strong{attr_selector}"
target_elements = tree.xpath(xpath_query)
for el in target_elements:
# Get full text content inside the strong tag
raw_text = "".join(el.itertext()).strip()
if not raw_text:
continue
# 2. Trim trailing punctuation (e.g., colons, periods, dashes)
clean_title_text = raw_text.rstrip(string.punctuation).strip()
# 3. Identify the immediate <p> parent and the grandparent (usually <body>)
p_parent = el.getparent()
if p_parent is None or p_parent.tag != 'p':
continue # Fallback if structure deviates unexpectedly
grandparent = p_parent.getparent()
if grandparent is not None:
# 4. Create the new title element (e.g., <h2>)
new_title_el = etree.Element(tag_name)
new_title_el.text = clean_title_text
new_title_el.set("class", "extracted-article-title")
# 5. Insert the new title element right BEFORE the parent <p> element
idx = grandparent.index(p_parent)
grandparent.insert(idx, new_title_el)
# Optional: If you want to remove the original <p><code><strong></code> block
# after extracting it to avoid duplication, you can do:
# grandparent.remove(p_parent)
return tree
Why this structure works cleanly:
- DOM Traversal: el.getparent() grabs the enclosing <p>, and p_parent.getparent() climbs up to <body> (the grandparent).
- Precise Placement: Using grandparent.insert(idx, new_title_el) places your new header tag (<h2> or similar) right above the <body><p>...</p> block, perfectly matching your required body > title and body > p sibling relationship.
UP: I need a seperate helper function to pre-check and ensure every strong[@data-text-type='type_1_article_title'] has a body>p>strong[@data-text-type='type_1_article_title'] structure.
Response: Here is a dedicated pre-check and validation helper function. It scans your HTML tree for any target <strong> tags that violate the expected body > p > strong structure and reports them (or logs anomalies) before you run your extraction logic.
def validate_type_1_structures(tree):
"""
Pre-checks every strong[@data-text-type='type_1_article_title'] element
to ensure its direct parent is a <p> tag and its grandparent is the <body> tag.
Returns a tuple: (is_valid: bool, invalid_elements: list)
"""
xpath_query = "//strong[@data-text-type='type_1_article_title']"
target_elements = tree.xpath(xpath_query)
invalid_elements = []
for el in target_elements:
p_parent = el.getparent()
# Check if parent exists and is a <p> tag
if p_parent is None or p_parent.tag.lower() != 'p':
invalid_elements.append(el)
continue
grandparent = p_parent.getparent()
# Check if grandparent exists and is the <body> tag
if grandparent is None or grandparent.tag.lower() != 'body':
invalid_elements.append(el)
is_valid = len(invalid_elements) == 0
return is_valid, invalid_elements
How to use it in your processing pipeline:
You can run this as a safety guard before attempting your title injection:
is_safe_to_process, anomalies = validate_type_1_structures(tree)
if not is_safe_to_process:
print(f"Warning: Found {len(anomalies)} structural anomalies where Type 1 titles are not wrapped in body > p > strong!")
# Optional: Send anomalies to your inline_title_anomaly_fixer module here
else:
# Proceed safely with your title extraction and injection
extract_and_inject_article_titles(tree)
UP: how to create alias/synonim for a function in Python
Response: In Python, creating an alias or synonym for a function is extremely simple because functions are first-class objects. You just assign the function object to a new variable name without using parentheses.
Here are the most common ways to do it:
1. Direct Assignment (The Standard Way)
You simply assign the function reference to a new name.
def extract_and_inject_article_titles(tree, tag_name="h2"):
# ... logic here ...
pass
# Create an alias/synonym
inject_titles = extract_and_inject_article_titles
# Both calls do the exact same thing
inject_titles(my_tree)
extract_and_inject_article_titles(my_tree)
2. Using an Alias for Imports
If you are importing a function from another module and want a shorter or alternative name in your current file, use the as keyword:
from strong_text_classifier_v2 import extract_and_inject_article_titles as inject_titles
# Now you can call it using the synonym
inject_titles(my_tree)
💡 Why this works:
In Python, function names are just labels pointing to a function object in memory. When you write alias = original_function, you are just creating a second label that points to that exact same block of code.
UP: now, since input file has predictible structure defined by series of body>titles let's tokenize article elements. Everything starting from <title> element (inclusively) till another <title> element (exclusively) should be placed inside <page> element.
Response: To tokenize your article elements into clean container blocks, you can iterate through the direct children of the <body> element. Whenever your newly injected <title> (or <h2>) element is encountered, it triggers the creation of a new <page> element, which consumes all subsequent sibling elements until the next title appears.
Here is how you can implement this DOM tokenization cleanly using lxml:
Article Page Tokenizer Function
from lxml import etree
def tokenize_articles_by_title(tree, title_tag_name="h2", wrapper_tag_name="page"):
"""
Groups elements under <body> into <page> elements.
Each <page> starts with a <title> element and includes everything
up until the next <title> element.
"""
body = tree.find("body")
if body is None:
return tree
# Get all direct children of the body
children = list(body)
# Create a new root container for our tokenized structure
new_body_elements = []
current_page = None
for el in children:
# Check if this element is our article title (matching the tag name)
if el.tag == title_tag_name:
# If a page was already open, close it by appending to our collection
if current_page is not None:
new_body_elements.append(current_page)
# Create a brand new <page> element
current_page = etree.Element(wrapper_tag_name)
# Append the title element *inside* the new page container
# (or leave it as a direct child if you want it to wrap the group)
current_page.append(el)
else:
# If we encounter content before the very first title, handle gracefully
if current_page is None:
new_body_elements.append(el)
else:
# Append subsequent sibling elements into the active page container
current_page.append(el)
# Don't forget to append the final open page loop
if current_page is not None:
new_body_elements.append(current_page)
# Clear the old body children and replace them with our new tokenized elements
body.clear()
for item in new_body_elements:
body.append(item)
return tree
How this transforms your HTML layout:
Before Tokenization:
<body>
<h2>Article Title 1</h2>
<p>Paragraph 1 under article 1</p>
<p>Paragraph 2 under article 1</p>
<h2>Article Title 2</h2>
<p>Paragraph 1 under article 2</p>
</body>
After Tokenization:
<body>
<page>
<h2>Article Title 1</h2>
<p>Paragraph 1 under article 1</p>
<p>Paragraph 2 under article 1</p>
</page>
<page>
<h2>Article Title 2</h2>
<p>Paragraph 1 under article 2</p>
</page>
</body>
You can chain this right after your extract_and_inject_article_titles function runs in your pipeline!
UP: Now I have articles tokenized as page elements according to MediaWiki Export schema. Every page has title element and following definition not encapsulated yet: page>title and following title siblings. Let's put all the siblings after title to dedicated block called <text>.
Response: To match the standard MediaWiki Export schema where all content following the page's <title> element is wrapped inside a <text> element, we can update our tokenization logic.
Inside each <page> container, the <title> should remain a direct child, while all subsequent sibling elements get gathered and encapsulated inside a new <text> wrapper.
Here is the function to structure your tokenized pages into the MediaWiki schema format:
MediaWiki Schema Grouping Function
from lxml import etree
def encapsulate_page_content_to_text(tree, page_tag="page", title_tag="h2", text_wrapper_tag="text"):
"""
Takes tokenized <page> elements and encapsulates all siblings
following the <title> into a dedicated <text> block.
Resulting structure: <page><h2/><text><p>...</p></text></page>
"""
pages = tree.xpath(f"//{page_tag}")
for page in pages:
# Get all children currently inside this page
children = list(page)
if not children:
continue
# The first element is expected to be our title
title_el = None
other_children = []
for i, el in enumerate(children):
if el.tag == title_tag and title_el is None:
title_el = el
# Everything after this title element belongs in the text block
other_children = children[i+1:]
break
# If we successfully found a title and have content to wrap
if title_el is not None and other_children:
# 1. Create the <text> container element
text_el = etree.Element(text_wrapper_tag)
# 2. Move all sibling elements into the new <text> block
for el in other_children:
text_el.append(el) # .append() automatically removes el from its old parent (page)
# 3. Rebuild the page contents cleanly: <page> -> [<title>, <text>]
page.clear()
page.append(title_el)
page.append(text_el)
return tree
How the DOM transforms:
Before Encapsulation:
<page>
<h2>Article Title 1</h2>
<p>Paragraph 1 under article 1</p>
<p>Paragraph 2 under article 1</p>
</page>
After Encapsulation (MediaWiki Schema):
<page>
<h2>Article Title 1</h2>
<text>
<p>Paragraph 1 under article 1</p>
<p>Paragraph 2 under article 1</p>
</text>
</page>
You can seamlessly append this function right after your tokenize_articles_by_title step in your processing pipeline.
UP: I don't use h2, but directly <title>
Response: Using <title> directly makes your schema even closer to native MediaWiki format!
Since the tag name is now title, we just update the parameters or default tag name in our functions. Here is the adjusted encapsulation function configured to look for <title> instead of <h2>:
MediaWiki Schema Grouping Function (with <title>)
from lxml import etree
def encapsulate_page_content_to_text(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Takes tokenized <page> elements and encapsulates all siblings
following the <title> into a dedicated <text> block.
Resulting structure: <page><title/><text><p>...</p></text></page>
"""
pages = tree.xpath(f"//{page_tag}")
for page in pages:
children = list(page)
if not children:
continue
title_el = None
other_children = []
for i, el in enumerate(children):
if el.tag == title_tag and title_el is None:
title_el = el
other_children = children[i+1:]
break
if title_el is not None and other_children:
text_el = etree.Element(text_wrapper_tag)
for el in other_children:
text_el.append(el) # Automatically moves it from page into text
page.clear()
page.append(title_el)
page.append(text_el)
return tree
Resulting DOM Structure:
<page>
<title>Article Title 1</title>
<text>
<p>Paragraph 1 under article 1</p>
<p>Paragraph 2 under article 1</p>
</text>
</page>
Make sure your title extraction/injection step is also creating a <title> element instead of an <h2> when it runs prior to this tokenization block!
UP: what if to name this function tokenize_text?
Response: That is a clean, descriptive name. Renaming it to tokenize_text accurately reflects that it processes the flat document text stream and structures it into tokenized containers.
Here is the updated function with the new name and configured for the <title> tag:
from lxml import etree
def tokenize_text(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Groups elements under <body> into <page> elements starting with a <title>,
and encapsulates all subsequent siblings into a dedicated <text> block.
Resulting structure: <page><title/><text><p>...</p></text></page>
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
for el in children:
# Check if this element is our article title
if el.tag == title_tag:
# If a page was already open, close and store it
if current_page is not None:
new_body_elements.append(current_page)
# Create a brand new <page> element and put the title inside it
current_page = etree.Element(page_tag)
current_page.append(el)
else:
# If content appears before the first title, handle it gracefully
if current_page is None:
new_body_elements.append(el)
else:
# Instead of appending directly to page, we ensure a <text> block exists
# Let's organize the page structure: <page> -> <title>, then <text> wrapping the rest
pass # Handled cleanly below or during a secondary sweep
Wait, let's look at the implementation to combine both steps (grouping by title + wrapping siblings into <text>) cleanly into a single unified tokenize_text function so you don't have to manage two separate loops!
The Unified tokenize_text Function:
from lxml import etree
def tokenize_text(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements into MediaWiki-style <page> containers,
where each page contains a <title> followed by a <text> block holding its content.
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_text = None
for el in children:
if el.tag == title_tag:
# Finalize the previous page if it exists
if current_page is not None:
if current_text is not None and len(current_text) > 0:
current_page.append(current_text)
new_body_elements.append(current_page)
# Start a new page and text container
current_page = etree.Element(page_tag)
current_page.append(el) # Append <title>
current_text = etree.Element(text_wrapper_tag)
else:
if current_page is None:
# Content before any title occurs
new_body_elements.append(el)
else:
# Append content siblings into the active <text> block
current_text.append(el)
# Don't forget to push the final page loop
if current_page is not None:
if current_text is not None and len(current_text) > 0:
current_page.append(current_text)
new_body_elements.append(current_page)
# Rebuild body children
body.clear()
for item in new_body_elements:
body.append(item)
return tree
Why this approach shines:
- Single Pass: It tokenizes the pages and encapsulates the sibling text blocks in one clean execution loop.
- Descriptive Name: tokenize_text clearly conveys its role in mapping raw HTML into structured schema nodes.
UP: I think tokenize_text name doesn't reflects the actual function, since it creates page elements too, not only text elements
Response: You are completely right. tokenize_text implies it's just dealing with text nodes, whereas it's actually orchestrating a structural transformation into full-fledged pages.
A more accurate and descriptive name would be tokenize_pages or wrap_into_pages.
Here is the finalized function updated with the clearer name tokenize_pages:
from lxml import etree
def tokenize_pages(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements into MediaWiki-style <page> containers,
where each page contains a <title> followed by a <text> block holding its content.
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_text = None
for el in children:
if el.tag == title_tag:
# Finalize the previous page if it exists
if current_page is not None:
if current_text is not None and len(current_text) > 0:
current_page.append(current_text)
new_body_elements.append(current_page)
# Start a new page and text container
current_page = etree.Element(page_tag)
current_page.append(el) # Append <title>
current_text = etree.Element(text_wrapper_tag)
else:
if current_page is None:
# Content before any title occurs
new_body_elements.append(el)
else:
# Append content siblings into the active <text> block
current_text.append(el)
# Don't forget to push the final page loop
if current_page is not None:
if current_text is not None and len(current_text) > 0:
current_page.append(current_text)
new_body_elements.append(current_page)
# Rebuild body children
body.clear()
for item in new_body_elements:
body.append(item)
return tree
Using tokenize_pages instantly tells anyone reading your pipeline code that this step builds the <page> boundaries and organizes their internal content blocks.
UP: does tokenization mean organizing inner structer?
Response: Yes, exactly.
While the word tokenization is often used in Natural Language Processing (NLP) to mean breaking text down into individual words or subwords, in the context of data parsing and document engineering, it fundamentally means converting an unorganized or flat stream of data into discrete, structured building blocks (tokens or containers).
In your pipeline, tokenize_pages does two key things regarding inner structure:
- Segments the stream: It looks at a flat sequence of HTML elements and identifies where one logical boundary ends and another begins (using the <title> element as the anchor).
- Organizes hierarchy: It nests those boundaries into structured parent-child groupings (<page> → <title> + <text> → content siblings).
So, calling it tokenize_pages (or even structure_into_pages) is deeply accurate because it transforms a flat file layout into a uniform, nested schema.
Sources
- Tokenization in NLP: How It Works, Challenges, and Use Cases
UP: Is it ok to combine two functions together: 1. page creation; 2. page/text creation
Response: Yes, absolutely. In fact, combining them into a single pass (like the tokenize_pages function we just looked at) is much better for performance and code maintenance.
Here is why combining them is a great idea:
1. Performance (Single DOM Pass)
If you do them separately, your script has to loop through all the children of the <body> once to create the <page> elements, and then loop through them again (or query via XPath) to wrap the siblings into <text> tags. Combining them lets you build the entire structure in a single iteration (O(n) complexity).
2. Simpler Pipeline Orchestration
Keeping your pipeline clean means fewer moving parts in your main orchestrator script (Parse.py). Instead of chaining two closely related transformation functions, you just call one robust function that takes your flat body and outputs the complete MediaWiki schema:
# In Parse.py
tree = validate_and_fix_structure(tree)
tree = extract_and_inject_article_titles(tree)
tree = tokenize_pages(tree) # Handles both page creation and text wrapping in one pass!
As long as the function is well-documented (so anyone reading it knows it builds both the page boundaries and the internal text containers), combining them is standard, efficient practice for DOM manipulation pipelines!
UP: As long as u think it's propper to tokenize pages together with their inner children let me provide u what should be inside the page: <title>...</title> <revision> <timestamp>2025-12-28T08:18:52Z</timestamp> <contributor><username>KadyrM</username></contributor> <text>
Response: That adds the exact metadata required for a valid MediaWiki XML Export schema!
According to the official MediaWiki export format, the <text> element is actually a child of the <revision> element, which sits inside the <page> alongside the <title>.
Here is how your target structure looks now:
<page>
<title>Article Title</title>
<revision>
<timestamp>2025-12-28T08:18:52Z</timestamp>
<contributor>
<username>KadyrM</username>
</contributor>
<text>
</text>
</revision>
</page>
Updated tokenize_pages Function
Here is the updated function that automatically generates the <revision>, <timestamp>, and <contributor> metadata block, dynamically generates a current timestamp matching your format, and nests everything correctly:
from datetime import datetime, timezone
from lxml import etree
def tokenize_pages(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements into MediaWiki-style <page> containers,
wrapping content into <revision> -> <text> with standard metadata.
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_text = None
# Generate the standard timestamp and contributor metadata elements once per run
# Format: 2025-12-28T08:18:52Z (Using UTC timezone for consistency)
current_timestamp_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
for el in children:
if el.tag == title_tag:
# Finalize the previous page if it exists
if current_page is not None:
if current_text is not None and len(current_text) > 0:
current_revision.append(current_text)
new_body_elements.append(current_page)
# 1. Create <page> and add <title>
current_page = etree.Element(page_tag)
current_page.append(el)
# 2. Create <revision> block with metadata
current_revision = etree.Element("revision")
timestamp_el = etree.Element("timestamp")
timestamp_el.text = current_timestamp_str
current_revision.append(timestamp_el)
contributor_el = etree.Element("contributor")
username_el = etree.Element("username")
username_el.text = "KadyrM"
contributor_el.append(username_el)
current_revision.append(contributor_el)
# 3. Create <text> container for article body elements
current_text = etree.Element(text_wrapper_tag)
# Attach revision to page (we'll append current_text to revision later)
current_page.append(current_revision)
else:
if current_page is None:
# Content before any title occurs
new_body_elements.append(el)
else:
# Append content siblings into the active <text> block
current_text.append(el)
# Don't forget to push the final page loop
if current_page is not None:
if current_text is not None and len(current_text) > 0:
current_revision.append(current_text)
new_body_elements.append(current_page)
# Rebuild body children
body.clear()
for item in new_body_elements:
body.append(item)
return tree
What this accomplishes in a single pass:
- Valid Hierarchy: Accurately builds <page> $\rightarrow$ <revision> $\rightarrow$ <text> nesting.
- Standard Metadata: Automatically injects the precise timestamp format and hardcoded contributor (KadyrM) into every article revision block.
- Clean DOM Rebuilding: Keeps your pipeline efficient and fully compliant with the MediaWiki export format.
UP: i have forgot, everything inside text element should be embraced into <![CDATA[ ... ]]> block
Response: To wrap the content inside your <text> element within a <![CDATA[ ... ]]> block using lxml, you can use lxml.etree.CDATA().
In lxml, passing text through etree.CDATA() signals the serializer to output that node's inner content wrapped in the CDATA block.
Here is how you update your tokenize_pages function to inject the CDATA block into <text>:
from datetime import datetime, timezone
from lxml import etree
def tokenize_pages(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements into MediaWiki-style <page> containers,
wrapping inner content into <revision> -> <text> with CDATA formatting.
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_text = None
current_timestamp_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
for el in children:
if el.tag == title_tag:
# Finalize the previous page if it exists
if current_page is not None:
if current_text is not None and len(current_text) > 0:
current_revision.append(current_text)
new_body_elements.append(current_page)
# 1. Create <page> and add <title>
current_page = etree.Element(page_tag)
current_page.append(el)
# 2. Create <revision> block with metadata
current_revision = etree.Element("revision")
timestamp_el = etree.Element("timestamp")
timestamp_el.text = current_timestamp_str
current_revision.append(timestamp_el)
contributor_el = etree.Element("contributor")
username_el = etree.Element("username")
username_el.text = "KadyrM"
contributor_el.append(username_el)
current_revision.append(contributor_el)
# 3. Create <text> container
current_text = etree.Element(text_wrapper_tag)
current_page.append(current_revision)
else:
if current_page is None:
new_body_elements.append(el)
else:
# Append content siblings into the active <text> block
current_text.append(el)
# Push the final page loop
if current_page is not None:
if current_text is not None and len(current_text) > 0:
current_revision.append(current_text)
new_body_elements.append(current_page)
# Rebuild body children
body.clear()
for item in new_body_elements:
body.append(item)
# --- Wrap <text> contents in CDATA ---
for text_el in tree.xpath(f"//{text_wrapper_tag}"):
# Convert all inner HTML children of <text> to a serialized string chunk
inner_html_string = "".join(
etree.tostring(child, encoding="unicode", method="html")
for child in text_el
)
# Clear the element's actual child nodes and replace with CDATA string
text_el.clear()
text_el.text = etree.CDATA(inner_html_string)
return tree
What this update achieves:
- HTML Serialisation: It serializes all the raw HTML child elements (<p>, etc.) residing inside your <text> block into an HTML string format.
- CDATA Application: It replaces those child nodes with etree.CDATA(), forcing lxml to output them safely escaped inside <![CDATA[ ... ]]> when saving your final MediaWiki XML file.
UP: would it be possible to add the contents of text element into CDATA on the stage of building the revision/text or not? In this case only one loop would be needed.
Response: Yes, but with one important technical caveat regarding how lxml builds XML trees:
An XML element cannot simultaneously contain child DOM elements (like <p> tags) and a CDATA text block as direct children. A CDATA block is essentially a text node wrapper. If you put HTML tags inside a CDATA block, those tags must be serialized into a string first.
Because you are gathering multiple sibling elements (like paragraphs) as the loop runs, you can't easily append live lxml element objects into a CDATA container while you build it—they have to be converted to a string format once their sibling group is complete.
However, you can still achieve a clean, single-pass iteration by building the element structure normally during the loop, and then transforming the <text> content into CDATA right at the moment each page/revision block is finalized!
Here is how you can structure that single-pass implementation:
Optimized Single-Pass Implementation
from datetime import datetime, timezone
from lxml import etree
def tokenize_pages(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements into MediaWiki-style <page> containers
in a single pass, converting <text> contents into CDATA as pages finalize.
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_revision = None
current_text = None
current_timestamp_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def finalize_current_page():
"""Helper to serialize text children into CDATA and push the page."""
nonlocal current_page, current_revision, current_text
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# 1. Serialize all children inside <text> to an HTML string
inner_html_string = "".join(
etree.tostring(child, encoding="unicode", method="html")
for child in current_text
)
# 2. Clear elements and apply CDATA text
current_text.clear()
current_text.text = etree.CDATA(inner_html_string)
current_revision.append(current_text)
new_body_elements.append(current_page)
for el in children:
if el.tag == title_tag:
# Finalize the previous page before opening a new one
finalize_current_page()
# 1. Create <page> and add <title>
current_page = etree.Element(page_tag)
current_page.append(el)
# 2. Create <revision> block with metadata
current_revision = etree.Element("revision")
timestamp_el = etree.Element("timestamp")
timestamp_el.text = current_timestamp_str
current_revision.append(timestamp_el)
contributor_el = etree.Element("contributor")
username_el = etree.Element("username")
username_el.text = "KadyrM"
contributor_el.append(username_el)
current_revision.append(contributor_el)
# 3. Create active <text> container tracker
current_text = etree.Element(text_wrapper_tag)
current_page.append(current_revision)
else:
if current_page is None:
# Content before any title occurs
new_body_elements.append(el)
else:
# Accumulate content siblings into the active <text> block
current_text.append(el)
# Finalize the very last page loop
finalize_current_page()
# Rebuild body children
body.clear()
for item in new_body_elements:
body.append(item)
return tree
Why this structure works best:
- Single Pass (O(n)): It loops through the body children exactly once.
- Encapsulated Helper: Using a nested finalize_current_page() helper handles the serialization and CDATA wrapping cleanly right at the moment a page boundary closes, keeping your main loop readable and free of code duplication.
UP: the code gave me such result: <page><title class="extracted-article-title">ЛАБАН</title><revision><timestamp>2026-08-27T07:54:37Z</timestamp><contributor><username>KadyrM</username></contributor><text> text content ]]></text></revision></page> mistake 1: mistake 2: ]]>
Response: Ah, those two mistakes happened because of how lxml serializes elements when it mixes HTML parsing modes with XML tree building:
- Mistake 1 (): lxml interpreted the method as HTML instead of XML, causing it to treat the CDATA tags as an HTML comment node.
- Mistake 2 (]]>): lxml automatically safety-escapes the closing ]]> characters inside text nodes to ]]> to prevent breaking standard XML parsers.
To fix both issues permanently and produce a clean <text><![CDATA[<p>text content</p>]]></text> block, we need to specify method="xml" during serialization and ensure we handle the CDATA section correctly.
Here is the corrected snippet for your tokenize_pages function:
from datetime import datetime, timezone
from lxml import etree
def tokenize_pages(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements into MediaWiki-style <page> containers
in a single pass, wrapping inner content into clean XML CDATA blocks.
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_revision = None
current_text = None
current_timestamp_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def finalize_current_page():
nonlocal current_page, current_revision, current_text
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# Use method="xml" so tags don't break into HTML-style comments
inner_html_string = "".join(
etree.tostring(child, encoding="unicode", method="xml")
for child in current_text
)
# Clear elements and apply true XML CDATA
current_text.clear()
current_text.text = etree.CDATA(inner_html_string)
current_revision.append(current_text)
new_body_elements.append(current_page)
for el in children:
if el.tag == title_tag:
finalize_current_page()
current_page = etree.Element(page_tag)
current_page.append(el)
current_revision = etree.Element("revision")
timestamp_el = etree.Element("timestamp")
timestamp_el.text = current_timestamp_str
current_revision.append(timestamp_el)
contributor_el = etree.Element("contributor")
username_el = etree.Element("username")
username_el.text = "KadyrM"
contributor_el.append(username_el)
current_revision.append(contributor_el)
current_text = etree.Element(text_wrapper_tag)
current_page.append(current_revision)
else:
if current_page is None:
new_body_elements.append(el)
else:
current_text.append(el)
finalize_current_page()
body.clear()
for item in new_body_elements:
body.append(item)
return tree
Why this fixes it:
- Switching method="html" to method="xml" inside etree.tostring() ensures that the inner elements are serialized as strict XML (preventing broken comment-style CDATA tags).
- Using etree.CDATA() inside the single-pass finalization helper now cleanly yields <text><![CDATA[...]]></text> right when the page block closes.
Sources
- XML Escaping in Python - SSOJet
UP: mistake 1 is still persisting:
Response: The reason is still happening is because etree.tostring(child, method="xml") processes individual fragment elements out-of-context, causing lxml to misunderstand how to format internal HTML tags combined with CDATA objects.
To bypass this quirk and get a pristine block without manual string serialization glitches, we can use lxml.html.tostring() specifically for the inner contents, or construct the CDATA wrapper string manually before wrapping it in etree.CDATA().
Here is the direct fix for the finalize_current_page helper function:
from lxml import html, etree
def finalize_current_page():
nonlocal current_page, current_revision, current_text
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# Use lxml.html.tostring to safely serialize all child elements of <text>
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in current_text
)
# Clear elements and assign clean CDATA
current_text.clear()
current_text.text = etree.CDATA(inner_html_string)
current_revision.append(current_text)
new_body_elements.append(current_page)
Why this works:
- lxml.html.tostring(..., method="html") handles nested HTML tags (like <p>) correctly without confusing them for comments.
- Wrapping that exact compiled HTML string into etree.CDATA() will output <text><![CDATA[<p>text content</p>]]></text> precisely as required by MediaWiki.
UP: it turns out the browser was commenting out the <![CDATA[<p> while it is correct in the source
Response: Ah, that makes total sense! Browsers and standard HTML viewers love to swallow or misinterpret CDATA tags because they aren't native HTML constructs—they belong strictly to XML. When viewing an XML/HTML hybrid DOM tree directly in a browser inspector, it often misrenders <![CDATA[...]]> as an HTML comment block ().
Your code was actually outputting the correct XML syntax all along!
Wrapping Up Your Pipeline
Since your tokenize_pages function is now cleanly assembling:
- <page> and <title>
- <revision> with the correct <timestamp> (2026-08-27T07:54:37Z) and <contributor><username>KadyrM</username></contributor>
- A <text> node wrapping your serialized inner content inside <![CDATA[ ... ]]>
You have a complete, production-ready pipeline for converting your flat parsed HTML into a valid MediaWiki Export XML schema.
How would you like to proceed next? Are you ready to hook this up to your central Parse.py entry point to process full files, or is there another part of the export schema you'd like to refine?
UP: now i have my content packed into page/revision/text/CDATA nodes. I would like to improve the content by 1) encoding line break hyphenation to optional hyphenation a) inside a paragraph: [any letter][-][space][any letter] ==> For example, чыгар- масында ==> чыгармасында b) on the meeting point of two paragraphs: Example: <p> .... оазисин-</p><p>де ... </p> ==> <p> .... оазисинде ... </p>. Two paragraphs merged into one.
Response: To handle both types of line break hyphenation and merge the split paragraphs safely, we can add a post-processing text-cleanup step right after your pages are tokenized (or inside your pipeline before writing out the final XML).
Since your paragraph contents are serialized inside CDATA strings, applying regex pattern matching and replacement on the serialized HTML string before or after CDATA application is the most reliable approach.
Here is how you can implement these two specific rules:
Hyphenation Cleanup Function
import re
def clean_hyphenated_breaks(tree):
"""
1. Replaces intra-paragraph hyphenation: [letter]-[space][letter] ->
2. Merges cross-paragraph hyphenation: <p>...word-</p><p>word...</p> -> <p>...wordword...</p>
"""
# 1. Handle intra-paragraph hyphenations: letter- space letter
# We use a regex looking for unicode letter, hyphen, space, unicode letter
# Note: Using python's re with unicode support (\w or explicit Cyrillic/Latin ranges if needed)
intra_pattern = re.compile(r"([a-zA-Zа-яёА-ЯЁ])-\s+([a-zA-Zа-яёА-ЯЁ])")
# 2. Handle cross-paragraph hyphenations at tag boundaries
# Looks for a <p> (or any tag) ending with a word character and a hyphen, followed by an optional closing tag, whitespace, and an opening <p> with a word character.
cross_pattern = re.compile(r"([a-zA-Zа-яёА-ЯЁ])-</p>\s*<p>([a-zA-Zа-яёА-ЯЁ])")
# Traverse all text elements in the tree
for text_el in tree.xpath("//text"):
if text_el.text is not None:
# If it's wrapped in a CDATA object or is a plain string, extract its value
content = str(text_el.text)
# Apply cross-paragraph merge first (merges the two <p> tags into one with )
# Example: <p>...оазисин-</p><p>де...</p> becomes <p>...оазисинде...</p>
content = cross_pattern.sub(r"\1\2</p><p>", content) # Wait, let's look closer at paragraph merging:
return tree
Let's refine the cross-paragraph merge pattern so it cleanly merges the two <p> tags into a single unified paragraph block.
Refined Cross-Paragraph Merge Logic:
If you have: <p>... оазисин-</p><p>де ...</p> We want to strip out the closing </p> and opening <p> of the second block, insert , and join them into a single paragraph: <p>... оазисинде ...</p>
Here is the robust helper to handle both intra-paragraph and cross-paragraph hyphenation seamlessly:
import re
def process_hyphenations(html_content: str) -> str:
"""
Applies hyphenation fixes to HTML string content:
1. Intra-paragraph: 'чыгар- масында' -> 'чыгармасында'
2. Cross-paragraph: '<p>... оазисин-</p><p>де ...</p>' -> '<p>... оазисинде ...</p>'
"""
# Rule 1: Cross-paragraph hyphenation merge
# Matches: ending of first paragraph (letter + hyphen + </p>) followed optionally by whitespace,
# and beginning of next paragraph (<p> + letter)
cross_para_regex = re.compile(r"([a-zA-Zа-яёА-ЯЁ–—]+)-\s*</p>\s*<p>\s*([a-zA-Zа-яёА-ЯЁ–—]+)", re.UNICODE)
# We replace it by joining them under a single opening/closing <p> tag with
html_content = cross_para_regex.sub(r"<p>\1\2", html_content)
# Rule 2: Intra-paragraph hyphenation (letter - space letter)
intra_para_regex = re.compile(r"([a-zA-Zа-яёА-ЯЁ–—]+)-\s+([a-zA-Zа-яёА-ЯЁ–—]+)", re.UNICODE)
html_content = intra_para_regex.sub(r"\1\2", html_content)
return html_content
Where to plug this into your tokenize_pages workflow:
You can apply this cleanup right inside your finalize_current_page step, immediately before wrapping the serialized string into etree.CDATA():
def finalize_current_page():
nonlocal current_page, current_revision, current_text
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# 1. Serialize child elements to string
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in current_text
)
# 2. Fix hyphenations and merge split paragraphs
cleaned_html_string = process_hyphenations(inner_html_string)
# 3. Clear and wrap into CDATA
current_text.clear()
current_text.text = etree.CDATA(cleaned_html_string)
current_revision.append(current_text)
new_body_elements.append(current_page)
This ensures that whether a hyphenated word break happens safely inside a single paragraph or spans across a paragraph boundary, it gets cleanly unified into right inside your final CDATA export block.
UP: is there more reliable regex pattern for any Unicode letter like [L] u used before, since my text contains non Russian letters like өүң
Response: Yes! Python’s built-in re module unfortunately does not support native Unicode property escapes like \p{L}. If you try to use them with standard re, it will either error out or fail to match non-Latin characters like ө, ү, ң.
To make your regex fully Unicode-aware and correctly catch letters across alphabets (Cyrillic, extended characters like ө, ү, ң, Latin, etc.), you have two solid options:
Option 1: Use Python's built-in re with explicit Unicode character sets (\w flag or character ranges)
By default, Python 3’s re module matches alphanumeric characters and underscores based on Unicode when you pass the re.UNICODE flag (or by default in Python 3 for \w). However, \w includes numbers, so writing a custom character class using explicit character ranges or combining \w with bounds is safer.
For general Cyrillic and extended Turkic/Slavic letters (including ө, ү, ң, җ, Һ, etc.), you can expand your character class range explicitly, or target general word characters excluding numbers:
import re
def process_hyphenations(html_content: str) -> str:
"""
Applies hyphenation fixes using broad Unicode-compatible patterns.
\w in Python 3 matches Unicode word characters (letters, digits, underscore).
To target strictly letters including non-Russian extensions (ө, ү, ң),
we use comprehensive character ranges or negative lookups.
"""
# Using \w ensures it captures Cyrillic, Latin, and characters like ө, ү, ң, ӊ, etc.
# We explicitly exclude numbers by ensuring we match letter blocks.
# 1. Cross-paragraph hyphenation merge
# Matches words ending with a hyphen across </p><p> boundaries
cross_para_regex = re.compile(r"([^\W\d_]+)-\s*</p>\s*<p>\s*([^\W\d_]+)", re.UNICODE)
html_content = cross_para_regex.sub(r"<p>\1\2", html_content)
# 2. Intra-paragraph hyphenation (e.g., чыгар- масында)
intra_para_regex = re.compile(r"([^\W\d_]+)-\s+([^\W\d_]+)", re.UNICODE)
html_content = intra_para_regex.sub(r"\1\2", html_content)
return html_content
- [^\W\d_]+ is a powerful built-in trick in Python's re: it means "match any word character (\w), but not non-word characters (\W), not digits (\d), and not the underscore (_)". This leaves behind strictly letters from any language/alphabet supported by Python, including all your regional characters like ө, ү, ң.
Option 2: Use the external regex library (Supports true \p{L})
If you want to use genuine Unicode property shorthand like \p{L} (Letter) without building custom workarounds, you can install the drop-in replacement package called regex:
- Run terminal command: pip install regex
- Import it in your code:
import regex # instead of import re
def process_hyphenations(html_content: str) -> str:
# \p{L} matches any kind of letter from any language natively
cross_para_regex = regex.compile(r"([\p{L}]+)-\s*</p>\s*<p>\s*([\p{L}]+)")
html_content = cross_para_regex.sub(r"<p>\1\2", html_content)
intra_para_regex = regex.compile(r"([\p{L}]+)-\s+([\p{L}]+)")
html_content = intra_para_regex.sub(r"\1\2", html_content)
return html_content
Recommendation:
If you want to avoid installing external third-party packages, Option 1 ([^\W\d_]+) works out of the box using Python's standard library and safely targets all Unicode letters—ensuring words with ө, ү, ң merge and hyphenate properly.
Sources
- Python and regular expression with Unicode - regex - Stack Overflow
- Python regex matching Unicode properties - Stack Overflow
UP: if is optional hyphenation what entity is used for non-breakin hyphenation
Response: Unlike the soft/optional hyphen (), standard HTML does not have a short named entity (like or ) for a non-breaking hyphen.
Instead, you use its numeric decimal or hexadecimal character reference based on its Unicode code point (U+2011):
- Decimal Entity: ‑
- Hexadecimal Entity: ‑
In MediaWiki & XML Contexts:
If you want to prevent a compound word or a hyphenated term from breaking across lines during rendering, you can substitute the standard hyphen (-) with ‑ or ‑.
(Note: In MediaWiki markup specifically, editors sometimes also use a template like {{nbh}} or a Word Joiner U+2060 depending on how strict the rendering engine's font support is, but ‑ is the standard direct XML character code).
Sources
- Wikipedia:Non-breaking hyphen
- How to Use a Non-Breaking Hyphen ( ‑ ) in HTML - ladedu.com
UP: I think it would be a good practice to add a documentation note to the tokenize_pages() function, since it not only tokenizes but also performes hyphenation tweaks
Response: That is an excellent practice. Documenting functions that handle multiple transformation responsibilities prevents future confusion and keeps your pipeline maintainable.
Here is your fully refined tokenize_pages function, featuring updated Unicode-aware hyphenation logic, paragraph merging, and a clear docstring explaining its dual role:
from datetime import datetime, timezone
from lxml import html, etree
import re
def process_hyphenations(html_content: str) -> str:
"""
Applies Unicode-aware hyphenation fixes to HTML string content:
1. Cross-paragraph: '<p>... оазисин-</p><p>де ...</p>' -> '<p>... оазисинде ...</p>'
2. Intra-paragraph: 'чыгар- масында' -> 'чыгармасында'
"""
# Uses [^\W\d_]+ to match any Unicode letter (including ө, ү, ң, etc.) while ignoring numbers/symbols
cross_para_regex = re.compile(r"([^\W\d_]+)-\s*</p>\s*<p>\s*([^\W\d_]+)", re.UNICODE)
html_content = cross_para_regex.sub(r"<p>\1\2", html_content)
intra_para_regex = re.compile(r"([^\W\d_]+)-\s+([^\W\d_]+)", re.UNICODE)
html_content = intra_para_regex.sub(r"\1\2", html_content)
return html_content
def tokenize_pages(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements into MediaWiki-style <page> containers in a single pass.
Responsibilities:
1. Groups sequential elements starting from a <title> tag into a <page>.
2. Injects a MediaWiki <revision> block containing UTC timestamp and <contributor>.
3. Wraps the remaining sibling content elements into a <text> block.
4. Cleans and normalizes text by resolving intra-paragraph hyphenations and
merging cross-paragraph hyphenated breaks (using Unicode-aware matching for
extended characters like ө, ү, ң).
5. Serializes and escapes the final inner HTML content inside an XML CDATA block.
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_revision = None
current_text = None
current_timestamp_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def finalize_current_page():
nonlocal current_page, current_revision, current_text
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# Serialize child elements to string
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in current_text
)
# Apply hyphenation tweaks and merge split paragraphs
cleaned_html_string = process_hyphenations(inner_html_string)
# Clear and wrap into clean XML CDATA
current_text.clear()
current_text.text = etree.CDATA(cleaned_html_string)
current_revision.append(current_text)
new_body_elements.append(current_page)
for el in children:
if el.tag == title_tag:
finalize_current_page()
current_page = etree.Element(page_tag)
current_page.append(el)
current_revision = etree.Element("revision")
timestamp_el = etree.Element("timestamp")
timestamp_el.text = current_timestamp_str
current_revision.append(timestamp_el)
contributor_el = etree.Element("contributor")
username_el = etree.Element("username")
username_el.text = "KadyrM"
contributor_el.append(username_el)
current_revision.append(contributor_el)
current_text = etree.Element(text_wrapper_tag)
current_page.append(current_revision)
else:
if current_page is None:
new_body_elements.append(el)
else:
current_text.append(el)
finalize_current_page()
body.clear()
for item in new_body_elements:
body.append(item)
return tree
UP: the img elements become encapsulated inside CDATA. does it mean they are not visible for the parser any more
Response: Yes, exactly.
Once HTML elements like <img> (or <p>, <b>, etc.) are serialized into a string and wrapped inside a <![CDATA[ ... ]]> block, they are no longer live DOM nodes.
To an XML or DOM parser (like lxml), everything inside a CDATA section is treated strictly as raw character data (plain text). The parser will not traverse them, search them via XPath, or see them as element nodes; it just sees them as a string value held by the <text> element.
Why this is standard for MediaWiki XML:
This is actually the desired behavior for a MediaWiki XML export schema!
- MediaWiki stores the raw wikitext or raw HTML markup of an article's body as a string inside the <text> element.
- When you import this XML file into a MediaWiki database, MediaWiki reads that CDATA string, stores it in its database text table, and later renders the <img> tags on the webpage when a user views the article.
Is this what you want?
- If you are importing into MediaWiki: Yes, this is completely correct. MediaWiki expects the article body content to be a raw text/markup block inside CDATA so it can process it.
- If you still need to run DOM operations or xpath queries on those <img> elements after this step: You would need to perform those operations before the tokenize_pages function runs, because once they enter the CDATA block, they become text.
UP: In this case can u write me a separate function which would iterate through every text element and encapsulate the contents into CDATA. I think I will split page tokenization from CDATA serialization
Response: Splitting the CDATA serialization into its own dedicated function is a great architectural choice. It keeps your tokenize_pages function strictly focused on structural DOM organization, while a separate pass handles the final text wrapping.
Here is the decoupled wrap_text_in_cdata function, along with how it fits cleanly into your orchestration pipeline:
1. The Standalone CDATA Function
from lxml import html, etree
def wrap_text_in_cdata(tree, text_wrapper_tag="text"):
"""
Iterates through every <text> element in the tree, serializes its
child DOM nodes (like <p>, <img>, etc.) into an HTML string,
and wraps that string in an XML CDATA block.
"""
for text_el in tree.xpath(f"//{text_wrapper_tag}"):
if len(text_el) > 0:
# Serialize all child elements inside <text> to an HTML string
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in text_el
)
# Clear the live DOM nodes and replace with CDATA text
text_el.clear()
text_el.text = etree.CDATA(inner_html_string)
return tree
2. Updated tokenize_pages (Without CDATA)
Now, your tokenize_pages function handles structure, metadata injection, and hyphenation cleanup while keeping all inner elements live in the DOM:
from datetime import datetime, timezone
from lxml import etree
import re
def process_hyphenations(html_content: str) -> str:
"""
Applies Unicode-aware hyphenation fixes to HTML string content:
1. Cross-paragraph: '<p>... оазисин-</p><p>де ...</p>' -> '<p>... оазисинде ...</p>'
2. Intra-paragraph: 'чыгар- масында' -> 'чыгармасында'
"""
cross_para_regex = re.compile(r"([^\W\d_]+)-\s*</p>\s*<p>\s*([^\W\d_]+)", re.UNICODE)
html_content = cross_para_regex.sub(r"<p>\1\2", html_content)
intra_para_regex = re.compile(r"([^\W\d_]+)-\s+([^\W\d_]+)", re.UNICODE)
html_content = intra_para_regex.sub(r"\1\2", html_content)
return html_content
def tokenize_pages(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements into MediaWiki-style <page> containers in a single pass.
Responsibilities:
1. Groups sequential elements starting from a <title> tag into a <page>.
2. Injects a MediaWiki <revision> block containing UTC timestamp and <contributor>.
3. Wraps the remaining sibling content elements into a <text> block (kept live as DOM nodes).
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_revision = None
current_text = None
current_timestamp_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def finalize_current_page():
nonlocal current_page, current_revision, current_text
if current_page is not None:
if current_text is not None and len(current_text) > 0:
current_revision.append(current_text)
new_body_elements.append(current_page)
for el in children:
if el.tag == title_tag:
finalize_current_page()
current_page = etree.Element(page_tag)
current_page.append(el)
current_revision = etree.Element("revision")
timestamp_el = etree.Element("timestamp")
timestamp_el.text = current_timestamp_str
current_revision.append(timestamp_el)
contributor_el = etree.Element("contributor")
username_el = etree.Element("username")
username_el.text = "KadyrM"
contributor_el.append(username_el)
current_revision.append(contributor_el)
current_text = etree.Element(text_wrapper_tag)
current_page.append(current_revision)
else:
if current_page is None:
new_body_elements.append(el)
else:
current_text.append(el)
finalize_current_page()
body.clear()
for item in new_body_elements:
body.append(item)
return tree
3. How Your Orchestration Pipeline (Parse.py) Looks Now:
By separating them, you gain total flexibility—your <img> tags stay interactive and searchable until the very last step when you serialize for export:
# 1. Parse and clean structure
tree = parse_html_document(file_path)
# 2. Group into pages, inject metadata, and clean hyphenations
tree = tokenize_pages(tree)
# Optional: You can do other DOM/XPath checks on <img> tags here
# while elements are still live nodes!
# 3. Final step: Convert live text node contents into XML CDATA blocks
tree = wrap_text_in_cdata(tree)
# 4. Save to final MediaWiki XML file
xml_output = etree.tostring(tree, encoding="utf-8", xml_declaration=True, pretty_print=True)
UP: add process_hyphenations into page_tokenizer
Response: Here is the updated tokenize_pages function with process_hyphenations integrated back into it.
Since tokenize_pages now keeps the inner content as live DOM nodes while organizing the page/revision structure, applying hyphenation adjustments requires serializing the content to a string briefly, applying the regex tweaks, and then re-parsing it back into live DOM nodes so elements like <img> or <p> stay fully accessible to your parser!
from datetime import datetime, timezone
from lxml import html, etree
import re
def process_hyphenations(html_content: str) -> str:
"""
Applies Unicode-aware hyphenation fixes to HTML string content:
1. Cross-paragraph: '<p>... оазисин-</p><p>де ...</p>' -> '<p>... оазисинде ...</p>'
2. Intra-paragraph: 'чыгар- масында' -> 'чыгармасында'
"""
cross_para_regex = re.compile(r"([^\W\d_]+)-\s*</p>\s*<p>\s*([^\W\d_]+)", re.UNICODE)
html_content = cross_para_regex.sub(r"<p>\1\2", html_content)
intra_para_regex = re.compile(r"([^\W\d_]+)-\s+([^\W\d_]+)", re.UNICODE)
html_content = intra_para_regex.sub(r"\1\2", html_content)
return html_content
def tokenize_pages(tree, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements into MediaWiki-style <page> containers in a single pass.
Responsibilities:
1. Groups sequential elements starting from a <title> tag into a <page>.
2. Injects a MediaWiki <revision> block containing UTC timestamp and <contributor>.
3. Wraps the remaining sibling content elements into a <text> block (kept live as DOM nodes).
4. Applies Unicode-aware hyphenation tweaks and merges split paragraphs.
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_revision = None
current_text = None
current_timestamp_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def finalize_current_page():
nonlocal current_page, current_revision, current_text
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# 1. Serialize children to string to perform hyphenation & merging
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in current_text
)
# 2. Apply Unicode-aware hyphenation fixes
cleaned_html_string = process_hyphenations(inner_html_string)
# 3. Re-populate <text> with cleaned HTML so nodes (like <p>, <img>) remain live DOM elements
current_text.clear()
fragment = html.fragment_fromstring(cleaned_html_string, create_parent="div")
for child in list(fragment):
current_text.append(child)
current_revision.append(current_text)
new_body_elements.append(current_page)
for el in children:
if el.tag == title_tag:
finalize_current_page()
current_page = etree.Element(page_tag)
current_page.append(el)
current_revision = etree.Element("revision")
timestamp_el = etree.Element("timestamp")
timestamp_el.text = current_timestamp_str
current_revision.append(timestamp_el)
contributor_el = etree.Element("contributor")
username_el = etree.Element("username")
username_el.text = "KadyrM"
contributor_el.append(username_el)
current_revision.append(contributor_el)
current_text = etree.Element(text_wrapper_tag)
current_page.append(current_revision)
else:
if current_page is None:
new_body_elements.append(el)
else:
current_text.append(el)
finalize_current_page()
body.clear()
for item in new_body_elements:
body.append(item)
return tree
What this accomplishes:
- The hyphenation patterns are applied right when each page is finalized.
- Cross-paragraph and intra-paragraph breaks are resolved seamlessly.
- Because the cleaned HTML is re-parsed back into live DOM nodes, your <img> tags remain fully accessible for any subsequent processing steps before you finally invoke wrap_text_in_cdata at the very end of the pipeline.
UP: I need a separate function like process_hyphenations() which would: 1. take current text element contents, 2. find all images inside, and for each 3. access the title and keep it as the image_name = title + counter 4. save the inline images into specified path as image_name 5. leave the image_name as src attribute's value I plan to insert this function inside page_tokenizer() to reduce iterations
Response: Here is a dedicated function that extracts inline base64 images, saves them to your local disk, and updates the src attribute.
I've also included a quick title sanitizer, because article titles often contain spaces or characters that make for messy or invalid filenames.
1. The Image Processing Function
import os
import re
import base64
def process_inline_images(text_el, page_title, output_dir):
"""
Finds inline base64 images inside a <text> element, saves them to the output_dir
as '{page_title}_{counter}.{ext}', and updates the <img> src attribute.
"""
# Create directory if it doesn't exist
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Sanitize the title to create a safe file name (removes invalid path chars)
safe_title = re.sub(r'[\\/*?:"<>|]', "", page_title or "Untitled").strip().replace(" ", "_")
images = text_el.xpath(".//img")
for idx, img in enumerate(images, start=1):
src = img.get("src", "")
# Process only if it is a base64 inline image
if src.startswith("data:image"):
try:
# data:image/png;base64,iVBORw0KGgo...
header, encoded_data = src.split(",", 1)
# Extract extension (png, jpeg, gif, etc.)
ext = header.split(";")[0].split("/")[1]
if ext == "jpeg":
ext = "jpg"
image_name = f"{safe_title}_{idx}.{ext}"
image_path = os.path.join(output_dir, image_name)
# Decode and save to disk
img_data = base64.b64decode(encoded_data)
with open(image_path, "wb") as f:
f.write(img_data)
# Update the DOM node's src attribute to just the image name
img.set("src", image_name)
except Exception as e:
# If splitting or decoding fails, log or gracefully skip
print(f"Failed to process image {idx} for {safe_title}: {e}")
2. Integrating into tokenize_pages
By calling this inside finalize_current_page() before you serialize the elements for hyphenation cleanup, you ensure the heavy base64 strings are swapped out for clean, short filenames. This makes your string processing much faster and safer.
Here is the updated orchestrator:
from datetime import datetime, timezone
from lxml import html, etree
import re
# (Assume process_hyphenations and process_inline_images are defined here)
def tokenize_pages(tree, output_image_dir, page_tag="page", title_tag="title", text_wrapper_tag="text"):
"""
Tokenizes flat body elements, injects metadata, processes inline images,
and applies hyphenation cleanups.
"""
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_revision = None
current_text = None
current_title_str = ""
current_timestamp_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def finalize_current_page():
nonlocal current_page, current_revision, current_text, current_title_str
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# 1. Process inline images while elements are still live DOM nodes
process_inline_images(current_text, current_title_str, output_image_dir)
# 2. Serialize to string for hyphenation tweaks
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in current_text
)
# 3. Apply hyphenation fixes
cleaned_html_string = process_hyphenations(inner_html_string)
# 4. Re-populate <text> with cleaned DOM nodes
current_text.clear()
fragment = html.fragment_fromstring(cleaned_html_string, create_parent="div")
for child in list(fragment):
current_text.append(child)
current_revision.append(current_text)
new_body_elements.append(current_page)
for el in children:
if el.tag == title_tag:
finalize_current_page()
current_page = etree.Element(page_tag)
current_page.append(el)
# Capture title text for image naming
current_title_str = el.text or "Untitled"
current_revision = etree.Element("revision")
timestamp_el = etree.Element("timestamp")
timestamp_el.text = current_timestamp_str
current_revision.append(timestamp_el)
contributor_el = etree.Element("contributor")
username_el = etree.Element("username")
username_el.text = "KadyrM"
contributor_el.append(username_el)
current_revision.append(contributor_el)
current_text = etree.Element(text_wrapper_tag)
current_page.append(current_revision)
else:
if current_page is None:
new_body_elements.append(el)
else:
current_text.append(el)
finalize_current_page()
body.clear()
for item in new_body_elements:
body.append(item)
return tree
Key Additions:
- output_image_dir: Make sure to pass your target export path into tokenize_pages when you call it from your main Parse.py script (e.g., tokenize_pages(tree, "/path/to/media/folder")).
- current_title_str variable: Added to safely store the current page's title name so it can be passed over to the image processor to name files like Article_Name_1.png.
UP: img saved correctly but img src set in unclear manner like: src="%D0%9B%D0%90%D0%92%D0%A0%D0%95%D0%9D%D0%A2%D0%AC%D0%95%D0%92_1.jpg
Response: That behavior happens because the safe title string contains Cyrillic characters (e.g., ЛАВРЕНТЬЕВ), and when lxml or Python serializes the attribute value back into the HTML string, it URL-encodes non-ASCII characters by default (%D0%9B...).
For MediaWiki image links, you usually want the clean plaintext filename rather than percent-encoded text, or you can use a sanitized Latin/numeric fallback naming convention (or ensure the string is cleanly unquoted).
Here are two ways to fix or handle this:
Option A: Force plain unencoded text insertion
If you want the src attribute to hold the exact literal string (ЛАВРЕНТЬЕВ_1.jpg) instead of the URL-encoded version, you can ensure the filename uses ASCII-safe characters (like a transliteration or a fallback ID/slug), or let lxml write it directly. However, XML/HTML attributes natively url-encode certain characters depending on the serialization method.
A cleaner standard practice for MediaWiki exports (especially with Cyrillic or Turkic titles) is to generate an ASCII-safe unique identifier or hash for the image, while keeping the human-readable title in the MediaWiki markup if needed. But if you want to keep your current naming style, let's look at how to ensure it displays or saves cleanly.
Option B: Use an ASCII Slug or Transliteration for Filenames (Recommended)
Using URL-encoded filenames inside XML attributes can sometimes break local media parsers or file storage systems. A robust fix is to convert Cyrillic/Turkic titles into a clean ASCII equivalent slug (or use a counter/hash) for the actual file name and src attribute.
Here is a quick transliteration helper you can drop in to convert Cyrillic characters into safe Latin characters for filenames:
def transliterate_to_ascii(text: str) -> str:
"""Converts Cyrillic/Turkic characters to safe ASCII equivalents for filenames."""
cyrillic_to_latin = {
'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo', 'Ж': 'Zh',
'З': 'Z', 'И': 'I', 'Й': 'Y', 'К': 'K', 'Л': 'L', 'М': 'M', 'Н': 'N', 'О': 'O',
'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U', 'Ф': 'F', 'Х': 'Kh', 'Ц': 'Ts',
'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sch', 'Ъ': '', 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu',
'Я': 'Ya', 'Ө': 'O', 'Ү': 'U', 'Ң': 'N', 'Җ': 'Zh', 'Һ': 'H',
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh',
'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o',
'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts',
'ч': 'ch', 'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu',
'я': 'ya', 'ө': 'o', 'ü': 'u', 'ү': 'u', 'ң': 'n', 'җ': 'zh', 'һ': 'h'
}
return "".join(cyrillic_to_latin.get(char, char) for char in text)
Updated process_inline_images Using Transliteration:
Update your image processing function to use this transliteration step. This guarantees that your filenames and src attributes look clean, readable, and 100% free of %D0%9B percent-encoding:
import os
import re
import base64
def process_inline_images(text_el, page_title, output_dir):
"""
Finds inline base64 images, saves them using safe ASCII transliterated names,
and updates the <img> src attribute cleanly.
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 1. Transliterate title to ASCII to prevent percent-encoding in src attributes
ascii_title = transliterate_to_ascii(page_title or "Untitled")
# 2. Sanitize for file system safety
safe_title = re.sub(r'[\\/*?:"<>|]', "", ascii_title).strip().replace(" ", "_")
images = text_el.xpath(".//img")
for idx, img in enumerate(images, start=1):
src = img.get("src", "")
if src.startswith("data:image"):
try:
header, encoded_data = src.split(",", 1)
ext = header.split(";")[0].split("/")[1]
if ext == "jpeg":
ext = "jpg"
image_name = f"{safe_title}_{idx}.{ext}"
image_path = os.path.join(output_dir, image_name)
img_data = base64.b64decode(encoded_data)
with open(image_path, "wb") as f:
f.write(img_data)
# Sets clean src without URL encoding artifacts
img.set("src", image_name)
except Exception as e:
print(f"Failed to process image {idx} for {safe_title}: {e}")
This ensures your saved images have clean, professional filenames like LAVRENTYEV_1.jpg both on your disk and inside the XML attribute structure!
UP: in this case, let's not to create img elements with src attribute, but directly MediaWiki's [[File:image_name.ext | thumb | alt_text ]]. Create a different function
Response: Instead of maintaining <img> HTML tags in your DOM tree, you can use a dedicated function that searches for the image elements, extracts their base64 data, saves them to disk, and replaces the <img> node entirely with a MediaWiki-style file link string: [[File:filename.ext|thumb]].
Here is the new standalone function and how to integrate it into your pipeline.
1. The MediaWiki File Syntax Replacement Function
import os
import re
import base64
from lxml import etree
def transliterate_to_ascii(text: str) -> str:
"""Converts Cyrillic/Turkic characters to safe ASCII equivalents for filenames."""
cyrillic_to_latin = {
'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo', 'Ж': 'Zh',
'З': 'Z', 'И': 'I', 'Й': 'Y', 'К': 'K', 'Л': 'L', 'М': 'M', 'Н': 'N', 'О': 'O',
'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U', 'Ф': 'F', 'Х': 'Kh', 'Ц': 'Ts',
'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sch', 'Ъ': '', 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu',
'Я': 'Ya', 'Ө': 'O', 'Ү': 'U', 'Ң': 'N', 'Җ': 'Zh', 'Һ': 'H',
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh',
'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o',
'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts',
'ч': 'ch', 'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu',
'я': 'ya', 'ө': 'o', 'ü': 'u', 'ү': 'u', 'ң': 'n', 'җ': 'zh', 'һ': 'h'
}
return "".join(cyrillic_to_latin.get(char, char) for char in text)
def replace_images_with_mediawiki_syntax(text_el, page_title, output_dir):
"""
Finds inline base64 images inside a <text> element, saves them to disk,
and replaces the <img> element nodes with MediaWiki [[File:...]] markup.
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
ascii_title = transliterate_to_ascii(page_title or "Untitled")
safe_title = re.sub(r'[\\/*?:"<>|]', "", ascii_title).strip().replace(" ", "_")
images = text_el.xpath(".//img")
for idx, img in enumerate(images, start=1):
src = img.get("src", "")
if src.startswith("data:image"):
try:
header, encoded_data = src.split(",", 1)
ext = header.split(";")[0].split("/")[1]
if ext == "jpeg":
ext = "jpg"
image_name = f"{safe_title}_{idx}.{ext}"
image_path = os.path.join(output_dir, image_name)
# Save base64 data to disk
img_data = base64.b64decode(encoded_data)
with open(image_path, "wb") as f:
f.write(img_data)
# Construct MediaWiki syntax block
# You can customize parameters like 'thumb' or 'center' here
mw_syntax = f"[[File:{image_name}|thumb]]"
# Replace the <img> node safely in the lxml tree
parent = img.getparent()
if parent is not None:
# If the img is inside a paragraph or standalone, we can insert
# the wikitext snippet right before it, then drop the img node.
# Or attach it to the tail of the previous sibling if available.
prev = img.getprevious()
if prev is not None:
prev.tail = (prev.tail or "") + mw_syntax + (img.tail or "")
else:
parent.text = (parent.text or "") + mw_syntax + (img.tail or "")
parent.remove(img)
except Exception as e:
print(f"Failed to process image {idx} for {safe_title}: {e}")
2. Plug it into tokenize_pages
You can seamlessly substitute your old image extractor call inside tokenize_pages with this new function:
def finalize_current_page():
nonlocal current_page, current_revision, current_text, current_title_str
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# 1. Extract images, save to disk, and inject MediaWiki [[File:...]] text
replace_images_with_mediawiki_syntax(current_text, current_title_str, output_image_dir)
# 2. Serialize remaining elements to string for hyphenation tweaks
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in current_text
)
# 3. Apply hyphenation fixes
cleaned_html_string = process_hyphenations(inner_html_string)
# 4. Re-populate <text> with cleaned DOM nodes
current_text.clear()
fragment = html.fragment_fromstring(cleaned_html_string, create_parent="div")
for child in list(fragment):
current_text.append(child)
current_revision.append(current_text)
new_body_elements.append(current_page)
What this achieves:
Instead of leaving an HTML <img> tag behind in your DOM, the function intercepts the image node, saves the physical file as LAVRENTYEV_1.jpg, deletes the <img> element entirely from the DOM tree structure, and replaces it with native MediaWiki markup like [[File:LAVRENTYEV_1.jpg|thumb]].
UP: is possible to do this withour transliteration?
Response: Yes, absolutely. If you want to keep the original titles (including Cyrillic characters like ЛАВРЕНТЬЕВ or Turkic characters like ӨҮҢ) as the actual filename on disk and inside the MediaWiki markup without doing any transliteration, you can completely remove the transliteration step.
Python 3 natively supports Unicode file paths on all modern file systems (Windows, Linux, macOS), so saving files with Cyrillic names like ЛАВРЕНТЬЕВ_1.jpg works out of the box.
Here is the updated function without transliteration:
import os
import re
import base64
def replace_images_with_mediawiki_syntax(text_el, page_title, output_dir):
"""
Finds inline base64 images, saves them using the original (non-transliterated)
page title as the filename prefix, and replaces the <img> element with
MediaWiki [[File:...]] markup.
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Clean unsafe file system characters while preserving original Unicode letters
raw_title = page_title or "Untitled"
safe_title = re.sub(r'[\\/*?:"<>|]', "", raw_title).strip().replace(" ", "_")
images = text_el.xpath(".//img")
for idx, img in enumerate(images, start=1):
src = img.get("src", "")
if src.startswith("data:image"):
try:
header, encoded_data = src.split(",", 1)
ext = header.split(";")[0].split("/")[1]
if ext == "jpeg":
ext = "jpg"
# Uses the original Unicode title directly (e.g., ЛАВРЕНТЬЕВ_1.jpg)
image_name = f"{safe_title}_{idx}.{ext}"
image_path = os.path.join(output_dir, image_name)
# Save base64 data to disk
img_data = base64.b64decode(encoded_data)
with open(image_path, "wb") as f:
f.write(img_data)
# Construct MediaWiki syntax block
mw_syntax = f"[[File:{image_name}|thumb]]"
# Replace the <img> node safely in the lxml tree
parent = img.getparent()
if parent is not None:
prev = img.getprevious()
if prev is not None:
prev.tail = (prev.tail or "") + mw_syntax + (img.tail or "")
else:
parent.text = (parent.text or "") + mw_syntax + (img.tail or "")
parent.remove(img)
except Exception as e:
print(f"Failed to process image {idx} for {safe_title}: {e}")
Why this works smoothly:
- File System: Python handles saving files with names like ЛАВРЕНТЬЕВ_1.jpg natively on disk.
- MediaWiki Compatibility: MediaWiki natively supports and encourages UTF-8 titles and filenames (e.g., [[File:ЛАВРЕНТЬЕВ_1.jpg|thumb]]), so this aligns fully with standard MediaWiki formatting conventions. You can safely drop the transliterate_to_ascii helper entirely.
UP: MediaWiki img syntax has Caption field: [[File:image_name.ext | thumb | Caption]]. Can u put as its value alt attribute's value.
Response: import os
import re
import base64
def replace_images_with_mediawiki_syntax(text_el, page_title, output_dir):
"""
Finds inline base64 images inside a <text> element, saves them to disk,
and replaces the <img> element nodes with MediaWiki [[File:name|thumb|alt_text]] markup.
"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
raw_title = page_title or "Untitled"
safe_title = re.sub(r'[\\/*?:"<>|]', "", raw_title).strip().replace(" ", "_")
images = text_el.xpath(".//img")
for idx, img in enumerate(images, start=1):
src = img.get("src", "")
if src.startswith("data:image"):
try:
header, encoded_data = src.split(",", 1)
ext = header.split(";")[0].split("/")[1]
if ext == "jpeg":
ext = "jpg"
image_name = f"{safe_title}_{idx}.{ext}"
image_path = os.path.join(output_dir, image_name)
img_data = base64.b64decode(encoded_data)
with open(image_path, "wb") as f:
f.write(img_data)
# Extract alt attribute to use as the caption, default to empty or image_name if missing
alt_text = img.get("alt", "").strip()
# Construct MediaWiki syntax block with caption field
if alt_text:
mw_syntax = f"[[File:{image_name}|thumb|{alt_text}]]"
else:
mw_syntax = f"[[File:{image_name}|thumb]]"
parent = img.getparent()
if parent is not None:
prev = img.getprevious()
if prev is not None:
prev.tail = (prev.tail or "") + mw_syntax + (img.tail or "")
else:
parent.text = (parent.text or "") + mw_syntax + (img.tail or "")
parent.remove(img)
except Exception as e:
print(f"Failed to process image {idx} for {safe_title}: {e}")
UP: before replace_images_with_mediawiki_syntax() i need to update img elements' alt attributes. As u remember I have div[imageCaption] elements. It should contain two child nodes: p and img. U should Take the contents of p and put it as a value of alt attribute. remove the original p
Response: To link your caption containers directly to the images before converting them into MediaWiki syntax, we can run a quick preprocessing sweep.
This new helper function looks for your div caption blocks, grabs the text out of the inner <p>, assigns it to the sibling <img>'s alt attribute, and then safely deletes the redundant <p> element so only the image remains (ready to be converted by your file-replacement step).
The Image Caption Assigner Function
def assign_captions_to_images(text_el, caption_div_attr="imageCaption"):
"""
Finds <div> elements matching the caption container, extracts text from
their inner <p> tag, assigns it to the <img> tag's 'alt' attribute,
and removes the original <p> node.
"""
# Find all divs functioning as caption wrappers
# (Adjust xpath if your structure differs slightly, e.g., matching class or attribute)
caption_divs = text_el.xpath(f".//div[@{caption_div_attr}] | .//div[contains(@class, '{caption_div_attr}')]")
for div in caption_divs:
img = div.find("img")
p = div.find("p")
if img is not None and p is not None:
# Get text content from the <p> tag and assign it as the image alt attribute
caption_text = "".join(p.itertext()).strip()
if caption_text:
img.set("alt", caption_text)
# Remove the now-redundant <p> element
div.remove(p)
How to Integrate It Into Your Pipeline
You should invoke assign_captions_to_images() right at the start of your page finalization block, immediately before calling replace_images_with_mediawiki_syntax():
def finalize_current_page():
nonlocal current_page, current_revision, current_text, current_title_str
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# 1. Map caption <p> contents into <img> alt attributes and clean up <p>
assign_captions_to_images(current_text)
# 2. Extract base64 images, save to disk, and replace <img> with [[File:...]]
replace_images_with_mediawiki_syntax(current_text, current_title_str, output_image_dir)
# 3. Serialize remaining elements to string for hyphenation tweaks
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in current_text
)
# 4. Apply hyphenation fixes
cleaned_html_string = process_hyphenations(inner_html_string)
# 5. Re-populate <text> with cleaned DOM nodes
current_text.clear()
fragment = html.fragment_fromstring(cleaned_html_string, create_parent="div")
for child in list(fragment):
current_text.append(child)
current_revision.append(current_text)
new_body_elements.append(current_page)
Why this pipeline order works best:
- assign_captions_to_images runs while the DOM is live, shifting the text from <p> into the image's alt attribute.
- replace_images_with_mediawiki_syntax picks up that updated alt attribute and outputs the final MediaWiki markup: [[File:name.ext|thumb|Your Caption Here]].
- process_hyphenations wraps up text formatting cleanly before final entry into the revision stack.
UP: u are right it's not an attribute but a class value, the imageCaption
Response: Got it! Since imageCaption is a class value rather than an HTML attribute, we can target it cleanly using XPath's contains(@class, 'imageCaption') or a precise token match.
Here is the corrected assign_captions_to_images function that looks for div elements by their class name:
def assign_captions_to_images(text_el, caption_class_name="imageCaption"):
"""
Finds <div> elements with the specified class, extracts text from
their inner <p> tag, assigns it to the <img> tag's 'alt' attribute,
and removes the redundant <p> node.
"""
# Use XPath to safely find divs containing the class name
caption_divs = text_el.xpath(f".//div[contains(concat(' ', normalize-space(@class), ' '), ' {caption_class_name} ')]")
for div in caption_divs:
img = div.find("img")
p = div.find("p")
if img is not None and p is not None:
# Extract clean text from the paragraph
caption_text = "".join(p.itertext()).strip()
if caption_text:
img.set("alt", caption_text)
# Remove the now-unneeded <p> element from the div
div.remove(p)
Why this XPath variation is safer:
Using contains(concat(' ', normalize-space(@class), ' '), ' imageCaption ') prevents partial class matches (for example, it won't accidentally trigger on a class named myimageCaption or imageCaptionExtra), ensuring it strictly targets elements where imageCaption is a distinct class token.
Drop this into your pipeline right before your image-replacement step, and it will cleanly pull your captions into the images' alt attributes!
UP: only imgs inside div[@imageCaption] should be assigned alt text. All the other imgs should not have alt attribute.
Response: def assign_captions_to_images(text_el, caption_class_name="imageCaption"):
"""
Finds <div> elements with the specified class, extracts text from
their inner <p> tag, assigns it exclusively to the <img> tag's 'alt' attribute
within that div, and removes the redundant <p> node. Other <img> tags remain untouched.
"""
caption_divs = text_el.xpath(f".//div[contains(concat(' ', normalize-space(@class), ' '), ' {caption_class_name} ')]")
for div in caption_divs:
img = div.find("img")
p = div.find("p")
if img is not None and p is not None:
caption_text = "".join(p.itertext()).strip()
if caption_text:
img.set("alt", caption_text)
div.remove(p)
# Explicitly clear alt attributes on all other <img> elements outside caption blocks
all_images = text_el.xpath(".//img")
caption_images = set(caption_divs) # Or track via parent relationship
for img in all_images:
parent_div = img.getparent()
is_in_caption = False
while parent_div is not None and parent_div != text_el:
classes = parent_div.get("class", "")
if caption_class_name in classes.split():
is_in_caption = True
break
parent_div = parent_div.getparent()
if not is_in_caption and "alt" in img.attrib:
del img.attrib["alt"]
UP: Inside finalize_page alongside with hyphenation fix and image save i need to add wiki category. Can u write me a code which would ask what i want to append to text via dialog box
Response: To prompt you for a category via a dialog box and append it to the article's text during finalization, you can use Python's built-in tkinter library. It opens a lightweight native GUI prompt without requiring external packages.
Here is how to integrate the category prompt and append it as a MediaWiki category link ([[Category:...]]) inside your tokenize_pages flow:
1. The Category Prompt Helper
import tkinter as tk
from tkinter import simpledialog
def prompt_for_wiki_category(page_title: str) -> str:
"""
Opens a native dialog box asking the user for a MediaWiki category
to append to the current page.
"""
# Initialize a hidden root window so only the dialog pops up
root = tk.Tk()
root.withdraw()
# Prompt user via a small input box
category_input = simpledialog.askstring(
"Add MediaWiki Category",
f"Enter category to append for page '{page_title}':",
initialvalue=""
)
root.destroy()
if category_input and category_input.strip():
# Format as standard MediaWiki category syntax
return f"\n\n[[Category:{category_input.strip()}]]"
return ""
2. Integration into tokenize_pages
Call this function inside your finalize_current_page() step, right after processing your images and text. You can append the category string directly onto the serialized HTML string before re-parsing it or writing it into the DOM:
def finalize_current_page():
nonlocal current_page, current_revision, current_text, current_title_str
if current_page is not None:
if current_text is not None and len(current_text) > 0:
# 1. Assign captions to specific images & clear others' alt tags
assign_captions_to_images(current_text)
# 2. Extract base64 images, save to disk, and replace <img> with [[File:...]]
replace_images_with_mediawiki_syntax(current_text, current_title_str, output_image_dir)
# 3. Serialize remaining elements to string for hyphenation & category injection
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in current_text
)
# 4. Apply hyphenation fixes
cleaned_html_string = process_hyphenations(inner_html_string)
# 5. Prompt user for an optional category via dialog box and append it
wiki_category = prompt_for_wiki_category(current_title_str)
if wiki_category:
cleaned_html_string += wiki_category
# 6. Re-populate <text> with cleaned DOM nodes (including category markup)
current_text.clear()
fragment = html.fragment_fromstring(cleaned_html_string, create_parent="div")
for child in list(fragment):
current_text.append(child)
current_revision.append(current_text)
new_body_elements.append(current_page)
How this works:
When your pipeline hits a page boundary, a small system input dialog will pop up showing the current page title. Whatever category name you type in (e.g., History or Local Biographies) will automatically be formatted as [[Category:History]] and appended neatly to the bottom of the article's text block before CDATA serialization.
UP: Oh, I don't need prompting on every page, but only once for whole input document
Response: import tkinter as tk
from tkinter import simpledialog
def prompt_for_global_wiki_category() -> str:
"""
Opens a native dialog box once to ask the user for a single MediaWiki category
to apply across the entire input document.
"""
root = tk.Tk()
root.withdraw()
category_input = simpledialog.askstring(
"Add Global MediaWiki Category",
"Enter category to append to all pages in this document:",
initialvalue=""
)
root.destroy()
if category_input and category_input.strip():
return f"\n\n[[Category:{category_input.strip()}]]"
return ""
To use this globally without prompting on every iteration, invoke prompt_for_global_wiki_category() once at the very beginning of your tokenize_pages function, store the resulting string in a variable, and append that same string during each page's finalization step:
def tokenize_pages(tree, output_image_dir, page_tag="page", title_tag="title", text_wrapper_tag="text"):
body = tree.find("body")
if body is None:
return tree
children = list(body)
new_body_elements = []
current_page = None
current_revision = None
current_text = None
current_title_str = ""
# Prompt the user ONCE for the whole document before looping through pages
global_category = prompt_for_global_wiki_category()
current_timestamp_str = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def finalize_current_page():
nonlocal current_page, current_revision, current_text, current_title_str
if current_page is not None:
if current_text is not None and len(current_text) > 0:
assign_captions_to_images(current_text)
replace_images_with_mediawiki_syntax(current_text, current_title_str, output_image_dir)
inner_html_string = "".join(
html.tostring(child, encoding="unicode", method="html")
for child in current_text
)
cleaned_html_string = process_hyphenations(inner_html_string)
# Append the global category if the user entered one
if global_category:
cleaned_html_string += global_category
current_text.clear()
fragment = html.fragment_fromstring(cleaned_html_string, create_parent="div")
for child in list(fragment):
current_text.append(child)
current_revision.append(current_text)
new_body_elements.append(current_page)
# ... rest of your loop structure remains identical